diff --git a/.clang-tidy b/.clang-tidy index 515cf5881a..8683b18a06 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -30,7 +30,8 @@ Checks: | readability-*, -readability-identifier-length, -readability-magic-numbers, - -readability-function-cognitive-complexity + -readability-function-cognitive-complexity, + -*-prefer-static-over-anonymous-namespace CheckOptions: - key: readability-identifier-naming.ClassCase @@ -73,5 +74,3 @@ CheckOptions: value: CamelCase - key: readability-identifier-naming.VariableCase value: camelBack - - key: misc-include-cleaner.IgnoreHeaders - value: pybind11/detail/.* diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ebff833461..6a7cfbfa7b 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,7 +12,7 @@ permissions: jobs: build-sdist: name: 🐍 Packaging - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-sdist.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-sdist.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 # Builds wheels on all supported platforms using cibuildwheel. # The wheels are uploaded as GitHub artifacts `dev-cibw-*` or `cibw-*`, depending on whether @@ -31,7 +31,7 @@ jobs: windows-2022, windows-11-arm, ] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97fc9ae26d..2dbc269405 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: jobs: change-detection: name: 🔍 Change - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-change-detection.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-change-detection.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 cpp-tests-ubuntu: name: 🇨‌ Test 🐧 @@ -30,7 +30,7 @@ jobs: - runs-on: ubuntu-24.04 compiler: gcc config: Debug - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -50,7 +50,7 @@ jobs: - runs-on: macos-14 compiler: clang config: Debug - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -71,7 +71,7 @@ jobs: - runs-on: windows-2022 compiler: msvc config: Debug - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -88,7 +88,7 @@ jobs: runs-on: [ubuntu-24.04, ubuntu-24.04-arm] compiler: [gcc, clang, clang-20, clang-21] config: [Release, Debug] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -105,7 +105,7 @@ jobs: runs-on: [macos-14, macos-15, macos-15-intel] compiler: [clang, clang-20, clang-21, gcc-14, gcc-15] config: [Release, Debug] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -123,7 +123,7 @@ jobs: runs-on: [windows-2022, windows-2025, windows-11-arm] compiler: [msvc, clang] config: [Release] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -133,7 +133,7 @@ jobs: name: 🇨‌ Coverage needs: change-detection if: fromJSON(needs.change-detection.outputs.run-cpp-tests) - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-coverage.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-coverage.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 permissions: contents: read id-token: write @@ -142,14 +142,14 @@ jobs: name: 🇨‌ Lint needs: change-detection if: fromJSON(needs.change-detection.outputs.run-cpp-linter) - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-linter.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-linter.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: cmake-args: -DBUILD_MQT_CORE_BENCHMARKS=ON -DBUILD_MQT_CORE_MLIR=ON -DBUILD_MQT_CORE_BINDINGS=ON clang-version: 21 build-project: true files-changed-only: true setup-python: true - install-pkgs: "pybind11==3.0.1" + install-pkgs: "nanobind==2.10.2" cpp-linter-extra-args: "-std=c++20" setup-mlir: true @@ -168,7 +168,7 @@ jobs: macos-14, windows-2022, ] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} @@ -176,7 +176,7 @@ jobs: 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@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-coverage.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 permissions: contents: read id-token: write @@ -190,7 +190,7 @@ jobs: fail-fast: false matrix: runs-on: [macos-15, windows-2025] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} @@ -198,15 +198,16 @@ jobs: 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@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-linter.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: + check-stubs: true enable-ty: true build-sdist: name: 🚀 CD needs: change-detection if: fromJSON(needs.change-detection.outputs.run-cd) - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-sdist.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-sdist.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 build-wheel: name: 🚀 CD @@ -224,7 +225,7 @@ jobs: windows-2022, windows-11-arm, ] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index 08f3e3798b..c4d3a6badc 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: runs-on: [ubuntu-24.04, macos-14, windows-2022] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-qiskit-upstream-tests.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-qiskit-upstream-tests.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: runs-on: ${{ matrix.runs-on }} setup-z3: true @@ -28,7 +28,7 @@ jobs: name: Create issue on failure needs: qiskit-upstream-tests if: ${{ always() }} - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-qiskit-upstream-issue.yml@654680ab77b3af8226a9f5cd9acfd157bd106f17 # v1.17.5 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-qiskit-upstream-issue.yml@2acb39781fa6ca7baa10c2a31a508cb0e2292d9e # v1.17.6 with: tests-result: ${{ needs.qiskit-upstream-tests.result }} permissions: diff --git a/.license-tools-config.json b/.license-tools-config.json index 55387c4477..1172b134ef 100644 --- a/.license-tools-config.json +++ b/.license-tools-config.json @@ -33,6 +33,7 @@ ".*\\.tex", "uv\\.lock", "py\\.typed", - ".*build.*" + ".*build.*", + "core_patterns.txt" ] } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1820c7550e..a99e0c02f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,6 +58,12 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal + # Check for license headers + - repo: https://github.com/emzeat/mz-lictools + rev: v2.9.0 + hooks: + - id: license-tools + # Ensure uv lock file is up-to-date - repo: https://github.com/astral-sh/uv-pre-commit rev: 0.9.18 @@ -101,12 +107,6 @@ repos: - id: blacken-docs additional_dependencies: [black==25.*] - # Check for license headers - - repo: https://github.com/emzeat/mz-lictools - rev: v2.9.0 - hooks: - - id: license-tools - # Clang-format the C++ part of the code base automatically - repo: https://github.com/pre-commit/mirrors-clang-format rev: v21.1.8 diff --git a/CHANGELOG.md b/CHANGELOG.md index e2614c6bd0..5519b06b6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Changed +- ♻️ Migrate Python bindings from `pybind11` to `nanobind` ([#1383]) ([**@denialhaag**], [**@burgholzer**]) +- 📦️ Provide Stable ABI wheels for Python 3.12+ ([#1383]) ([**@burgholzer**], [**@denialhaag**]) +- 🚚 Create dedicated `mqt.core.na` submodule to closely follow the structure of other submodules ([#1383]) ([**@burgholzer**]) - ✨ Add common definitions and utilities for QDMI ([#1355]) ([**@burgholzer**]) - 🚚 Move `NA` QDMI device in its right place next to other QDMI devices ([#1355]) ([**@burgholzer**]) - ♻️ Allow repeated loading of QDMI device library with potentially different session configurations ([#1355]) ([**@burgholzer**]) @@ -285,6 +288,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [#1385]: https://github.com/munich-quantum-toolkit/core/pull/1385 [#1384]: https://github.com/munich-quantum-toolkit/core/pull/1384 +[#1383]: https://github.com/munich-quantum-toolkit/core/pull/1383 [#1382]: https://github.com/munich-quantum-toolkit/core/pull/1382 [#1381]: https://github.com/munich-quantum-toolkit/core/pull/1381 [#1378]: https://github.com/munich-quantum-toolkit/core/pull/1378 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cd8e95518..86f93ccc8e 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ # Licensed under the MIT License # set required cmake version -cmake_minimum_required(VERSION 3.24...4.0) +cmake_minimum_required(VERSION 3.24...4.2) project( mqt-core @@ -47,7 +47,8 @@ if(BUILD_MQT_CORE_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() # check if this is the master project or used via add_subdirectory diff --git a/README.md b/README.md index b17ad7bd4a..130b320002 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,6 @@ The project relies on some external dependencies: - [boost/multiprecision](https://github.com/boostorg/multiprecision): A library for multiprecision arithmetic (used in the ZX package). - [nlohmann/json](https://github.com/nlohmann/json): A JSON library for modern C++. -- [pybind/pybind11_json](https://github.com/pybind/pybind11_json): A library for using `nlohmann::json` with `pybind11` (only used for creating the Python bindings). - [google/googletest](https://github.com/google/googletest): A testing framework for C++ (only used in tests). CMake will automatically look for installed versions of these libraries. If it does not find them, they will be fetched automatically at configure time via the [FetchContent](https://cmake.org/cmake/help/latest/module/FetchContent.html) module (check out the documentation for more information on how to customize this behavior). diff --git a/UPGRADING.md b/UPGRADING.md index 9b56d35c1d..803baa54e2 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -52,12 +52,24 @@ In the process, the `mqt-core-dd-compare` entry point as well as the `evaluation The `eval/dd_evaluation.py` script acts as a drop-in replacement for the previous CLI entry point. Since the `eval` directory is not part of the Python package, this functionality is only available via source installations or by cloning the repository. -### Removal of Python 3.13t wheels +### Python wheels +This release contains two changes to the distributed wheels. + +First, we have removed all wheels for Python 3.13t. Free-threading Python was introduced as an experimental feature in Python 3.13. It became stable in Python 3.14. -To conserve space on PyPI and to reduce the CD build times, we have removed all wheels for Python 3.13t from our CI. -We continue to provide wheels for the regular Python versions 3.10 to 3.14, as well as 3.14t. + +Second, for Python 3.12+, we are now providing Stable ABI wheels instead of separate version-specific wheels. +This was enabled by migrating our Python bindings from `pybind11` to `nanobind`. + +Both of these changes were made in the interest of conserving PyPI space and reducing CI/CD build times. +The full list of wheels now reads: + +- 3.10 +- 3.11 +- 3.12+ Stable ABI +- 3.14t ## [3.3.0] diff --git a/bindings/.clang-tidy b/bindings/.clang-tidy new file mode 100644 index 0000000000..72daa60469 --- /dev/null +++ b/bindings/.clang-tidy @@ -0,0 +1,6 @@ +InheritParentConfig: true +Checks: | + -*-avoid-c-arrays, + -cppcoreguidelines-owning-memory, + -misc-redundant-expression, + -*-pro-bounds-avoid-unchecked-container-access diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt index 4d11f4075a..848b5ad841 100644 --- a/bindings/CMakeLists.txt +++ b/bindings/CMakeLists.txt @@ -6,14 +6,7 @@ # # Licensed under the MIT License -# add the IR bindings package add_subdirectory(ir) - -# add the DD bindings package add_subdirectory(dd) - -# add the NA bindings package -add_subdirectory(na) - -# add the FoMaC bindings package add_subdirectory(fomac) +add_subdirectory(na) diff --git a/bindings/core_patterns.txt b/bindings/core_patterns.txt new file mode 100644 index 0000000000..55bcfafb27 --- /dev/null +++ b/bindings/core_patterns.txt @@ -0,0 +1,3 @@ +_hashable_values_: + +_unhashable_values_map_: diff --git a/bindings/dd/CMakeLists.txt b/bindings/dd/CMakeLists.txt index 9b9950402a..aa149adc3c 100644 --- a/bindings/dd/CMakeLists.txt +++ b/bindings/dd/CMakeLists.txt @@ -11,7 +11,7 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}-dd-bindings) file(GLOB_RECURSE DD_SOURCES **.cpp) # declare the Python module - add_mqt_python_binding( + add_mqt_python_binding_nanobind( CORE ${MQT_CORE_TARGET_NAME}-dd-bindings ${DD_SOURCES} diff --git a/bindings/dd/register_dd.cpp b/bindings/dd/register_dd.cpp index 5c7c3e4f30..64815fed6a 100644 --- a/bindings/dd/register_dd.cpp +++ b/bindings/dd/register_dd.cpp @@ -16,88 +16,172 @@ #include "dd/StateGeneration.hpp" #include "ir/QuantumComputation.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -// clang-format on - #include #include #include -#include +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // forward declarations -void registerVectorDDs(const py::module& mod); -void registerMatrixDDs(const py::module& mod); -void registerDDPackage(const py::module& mod); +void registerVectorDDs(const nb::module_& m); +void registerMatrixDDs(const nb::module_& m); +void registerDDPackage(const nb::module_& m); -struct Vector { - dd::CVec v; -}; +using Vector = nb::ndarray, nb::ndim<1>>; Vector getVector(const dd::vEdge& v, dd::fp threshold = 0.); -struct Matrix { - std::vector> data; - size_t n; -}; +using Matrix = nb::ndarray, nb::ndim<2>>; Matrix getMatrix(const dd::mEdge& m, size_t numQubits, dd::fp threshold = 0.); -PYBIND11_MODULE(MQT_CORE_MODULE_NAME, mod, py::mod_gil_not_used()) { +NB_MODULE(MQT_CORE_MODULE_NAME, m) { + nb::module_::import_("mqt.core.ir"); + // Vector Decision Diagrams - registerVectorDDs(mod); + registerVectorDDs(m); // Matrix Decision Diagrams - registerMatrixDDs(mod); + registerMatrixDDs(m); // DD Package - registerDDPackage(mod); + registerDDPackage(m); - mod.def( + m.def( "sample", [](const qc::QuantumComputation& qc, const size_t shots = 1024U, const size_t seed = 0U) { return dd::sample(qc, shots, seed); }, - "qc"_a, "shots"_a = 1024U, "seed"_a = 0U); - - mod.def( + "qc"_a, "shots"_a = 1024U, "seed"_a = 0U, + R"pb(Sample from the output distribution of a quantum computation. + +This function classically simulates the quantum computation and repeatedly samples from the output distribution. +It supports mid-circuit measurements, resets, and classical control. + +Args: + qc: The quantum computation. + shots: The number of samples to take. + If the quantum computation contains no mid-circuit measurements or resets, the circuit is simulated once and the samples are drawn from the final state. + Otherwise, the circuit is simulated once for each sample. + Defaults to 1024. + seed: The seed for the random number generator. + If set to a specific non-zero value, the simulation is deterministic. + If set to 0, the RNG is randomly seeded. + Defaults to 0. + +Returns: + A histogram of the samples. + Each sample is a bitstring representing the measurement outcomes of the qubits in the quantum computation. + The leftmost bit corresponds to the most significant qubit, that is, the qubit with the highest index (big-endian). + If the circuit contains measurements, only the qubits that are actively measured are included in the output distribution. + Otherwise, all qubits in the circuit are measured.)pb"); + + m.def( "simulate_statevector", [](const qc::QuantumComputation& qc) { - auto dd = std::make_unique(qc.getNqubits()); - auto in = makeZeroState(qc.getNqubits(), *dd); - const auto sim = dd::simulate(qc, in, *dd); + const auto dd = std::make_unique(qc.getNqubits()); + const auto in = makeZeroState(qc.getNqubits(), *dd); + const auto sim = simulate(qc, in, *dd); return getVector(sim); }, - "qc"_a); + "qc"_a, + R"pb(Simulate the quantum computation and return the final state vector. + +This function classically simulates the quantum computation and returns the state vector of the final state. +It does not support measurements, resets, or classical control. + +Since the state vector is guaranteed to be exponentially large in the number of qubits, this function is only suitable for small quantum computations. +Consider using the :func:`~mqt.core.dd.simulate` or the :func:`~mqt.core.dd.sample` functions, which never explicitly construct the state vector, for larger quantum computations. + +Notes: + This function internally constructs a :class:`~mqt.core.dd.DDPackage`, creates the zero state, and simulates the quantum computation via the :func:`simulate` function. + The state vector is then extracted from the resulting DD via the :meth:`~mqt.core.dd.VectorDD.get_vector` method. + +Args: + qc: The quantum computation. Must only contain unitary operations. + +Returns: + The state vector of the final state.)pb"); - mod.def( + m.def( "build_unitary", [](const qc::QuantumComputation& qc, const bool recursive = false) { - auto dd = std::make_unique(qc.getNqubits()); - auto u = recursive ? dd::buildFunctionalityRecursive(qc, *dd) - : dd::buildFunctionality(qc, *dd); + const auto dd = std::make_unique(qc.getNqubits()); + const auto u = recursive ? buildFunctionalityRecursive(qc, *dd) + : buildFunctionality(qc, *dd); return getMatrix(u, qc.getNqubits()); }, - "qc"_a, "recursive"_a = false); + "qc"_a, "recursive"_a = false, + R"pb(Build a unitary matrix representation of a quantum computation. + +This function builds a matrix representation of the unitary representing the functionality of a quantum computation. +This function does not support measurements, resets, or classical control, as the corresponding operations are non-unitary. + +Since the unitary matrix is guaranteed to be exponentially large in the number of qubits, this function is only suitable for small quantum computations. +Consider using the :func:`~mqt.core.dd.build_functionality` function, which never explicitly constructs the unitary matrix, for larger quantum computations. + +Notes: + This function internally constructs a :class:`~mqt.core.dd.DDPackage`, creates the identity matrix, and builds the unitary matrix via the :func:`~mqt.core.dd.build_functionality` function. + The unitary matrix is then extracted from the resulting DD via the :meth:`~mqt.core.dd.MatrixDD.get_matrix` method. + +Args: + qc: The quantum computation. Must only contain unitary operations. + recursive: Whether to build the unitary matrix recursively. + If set to True, the unitary matrix is built recursively by pairwise grouping the operations of the quantum computation. + If set to False, the unitary matrix is built by sequentially applying the operations of the quantum computation to the identity matrix. + Defaults to False. + +Returns: + The unitary matrix representing the functionality of the quantum computation.)pb"); + + m.def("simulate", &dd::simulate, "qc"_a, "initial_state"_a, "dd_package"_a, + R"pb(Simulate a quantum computation. + +This function classically simulates a quantum computation for a given initial state and returns the final state (represented as a DD). +Compared to the `sample` function, this function does not support measurements, resets, or classical control. +It only supports unitary operations. + +The simulation is effectively computed by sequentially applying the operations of the quantum computation to the initial state. + +Args: + qc: The quantum computation. Must only contain unitary operations. + initial_state: The initial state as a DD. Must have the same number of qubits as the quantum computation. + The reference count of the initial state is decremented during the simulation, so the caller must ensure that the initial state has a non-zero reference count. + dd_package: The DD package. Must be configured with a sufficient number of qubits to accommodate the quantum computation. - mod.def("simulate", &dd::simulate, "qc"_a, "initial_state"_a, "dd_package"_a); +Returns: + The final state as a DD. The reference count of the final state is non-zero and must be manually decremented by the caller if it is no longer needed.)pb"); - mod.def( + m.def( "build_functionality", [](const qc::QuantumComputation& qc, dd::Package& p, const bool recursive = false) { if (recursive) { - return dd::buildFunctionalityRecursive(qc, p); + return buildFunctionalityRecursive(qc, p); } - return dd::buildFunctionality(qc, p); + return buildFunctionality(qc, p); }, - "qc"_a, "dd_package"_a, "recursive"_a = false); + "qc"_a, "dd_package"_a, "recursive"_a = false, + R"pb(Build a functional representation of a quantum computation. + +This function builds a matrix DD representation of the unitary representing the functionality of a quantum computation. +This function does not support measurements, resets, or classical control, as the corresponding operations are non-unitary. + +Args: + qc: The quantum computation. + Must only contain unitary operations. + dd_package: The DD package. Must be configured with a sufficient number of qubits to accommodate the quantum computation. + recursive: Whether to build the functionality matrix recursively. + If set to True, the functionality matrix is built recursively by pairwise grouping the operations of the quantum computation. + If set to False, the functionality matrix is built by sequentially applying the operations of the quantum computation to the identity matrix. + Defaults to False. + +Returns: + The functionality as a DD. The reference count of the result is non-zero and must be manually decremented by the caller if it is no longer needed.)pb"); } } // namespace mqt diff --git a/bindings/dd/register_dd_package.cpp b/bindings/dd/register_dd_package.cpp index 4900e256dc..a25d942c6f 100644 --- a/bindings/dd/register_dd_package.cpp +++ b/bindings/dd/register_dd_package.cpp @@ -20,22 +20,17 @@ #include "ir/operations/NonUnitaryOperation.hpp" #include "ir/operations/Operation.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -#include -#include -// clang-format on - #include #include #include #include -#include +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -43,15 +38,22 @@ namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; + +using Vector = nb::ndarray, nb::ndim<1>>; +using Matrix = nb::ndarray, nb::ndim<2>>; +using SingleQubitMatrix = + nb::ndarray, nb::shape<2, 2>>; +using TwoQubitMatrix = + nb::ndarray, nb::shape<4, 4>>; namespace { + /// Recursive helper function to create a vector DD from a numpy array -dd::vCachedEdge makeDDFromVector( - dd::Package& p, - const py::detail::unchecked_reference, 1>& v, - const size_t startIdx, const size_t endIdx, const dd::Qubit level) { +dd::vCachedEdge makeDDFromVector(dd::Package& p, const Vector& v, + const size_t startIdx, const size_t endIdx, + const dd::Qubit level) { if (level == 0U) { const auto zeroSuccessor = dd::vCachedEdge::terminal(v(startIdx)); const auto oneSuccessor = dd::vCachedEdge::terminal(v(startIdx + 1)); @@ -67,11 +69,10 @@ dd::vCachedEdge makeDDFromVector( } /// Recursive helper function to create a matrix DD from a numpy array -dd::mCachedEdge makeDDFromMatrix( - dd::Package& p, - const py::detail::unchecked_reference, 2>& m, - const size_t rowStart, const size_t rowEnd, const size_t colStart, - const size_t colEnd, const dd::Qubit level) { +dd::mCachedEdge makeDDFromMatrix(dd::Package& p, const Matrix& m, + const size_t rowStart, const size_t rowEnd, + const size_t colStart, const size_t colEnd, + const dd::Qubit level) { if (level == 0U) { const auto zeroSuccessor = dd::mCachedEdge::terminal(m(rowStart, colStart)); const auto oneSuccessor = @@ -96,18 +97,47 @@ dd::mCachedEdge makeDDFromMatrix( } // namespace // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerDDPackage(const py::module& mod) { - auto dd = - py::class_>(mod, "DDPackage"); +void registerDDPackage(const nb::module_& m) { + auto dd = nb::class_( + m, "DDPackage", + R"pb(The central manager for performing computations on decision diagrams. + +It drives all computation on decision diagrams and maintains the necessary data structures for this purpose. +Specifically, it + +- manages the memory for the decision diagram nodes (Memory Manager), +- ensures the canonical representation of decision diagrams (Unique Table), +- ensures the efficiency of decision diagram operations (Compute Table), +- provides methods for creating quantum states and operations from various sources, +- provides methods for various operations on quantum states and operations, and +- provides means for reference counting and garbage collection. + +Notes: + It is undefined behavior to pass VectorDD or MatrixDD objects that were created with a different DDPackage to the methods of the DDPackage. + The only exception is the identity DD returned by identity(), which represents the global one-terminal and can be used with any DDPackage instance. + +Args: + num_qubits: The maximum number of qubits that the DDPackage can handle. + Mainly influences the size of the unique tables. + Can be adjusted dynamically using the `resize` method. + Since resizing the DDPackage can be expensive, it is recommended to choose a value that is large enough for the quantum computations that are to be performed, but not unnecessarily large. + Default is 32.)pb"); // Constructor - dd.def(py::init(), "num_qubits"_a = dd::Package::DEFAULT_QUBITS); + dd.def(nb::init(), "num_qubits"_a = dd::Package::DEFAULT_QUBITS); // Resizing the package - dd.def("resize", &dd::Package::resize, "num_qubits"_a); + dd.def("resize", &dd::Package::resize, "num_qubits"_a, + R"pb(Resize the DDPackage to accommodate a different number of qubits. + +Args: + num_qubits: The new number of qubits. + Must be greater than zero. + It is undefined behavior to resize the DDPackage to a smaller number of qubits and then perform operations on decision diagrams that are associated with qubits that are no longer present.)pb"); // Getting the number of qubits the package is configured for - dd.def_property_readonly("max_qubits", &dd::Package::qubits); + dd.def_prop_ro("max_qubits", &dd::Package::qubits, + "The maximum number of qubits that the DDPackage can handle."); ///------------------------------------------------------------------------/// /// Vector DD Generation @@ -116,65 +146,124 @@ void registerDDPackage(const py::module& mod) { dd.def( "zero_state", [](dd::Package& p, const size_t numQubits) { - return dd::makeZeroState(numQubits, p); + return makeZeroState(numQubits, p); }, "num_qubits"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + R"pb(Create the DD for the zero state :math:`| 0 \ldots 0 \rangle`. + +Args: + num_qubits: The number of qubits. + Must not be greater than the number of qubits the DDPackage is configured with. + +Returns: + The DD for the zero state. + The resulting state is guaranteed to have its reference count increased.)pb"); dd.def( "computational_basis_state", [](dd::Package& p, const size_t numQubits, const std::vector& state) { - return dd::makeBasisState(numQubits, state, p); + return makeBasisState(numQubits, state, p); }, "num_qubits"_a, "state"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); - - py::native_enum(mod, "BasisStates", "enum.Enum", - "Enumeration of basis states.") - .value("zero", dd::BasisStates::zero) - .value("one", dd::BasisStates::one) - .value("plus", dd::BasisStates::plus) - .value("minus", dd::BasisStates::minus) - .value("right", dd::BasisStates::right) - .value("left", dd::BasisStates::left) - .finalize(); + nb::keep_alive<0, 1>(), + R"pb(Create the DD for the computational basis state :math:`| b_{n - 1} \ldots b_0 \rangle`. + +Args: + num_qubits: The number of qubits. + Must not be greater than the number of qubits the DDPackage is configured with. + state: The state as a list of booleans. + Must be at least `num_qubits` long. + +Returns: + The DD for the computational basis state. + The resulting state is guaranteed to have its reference count increased.)pb"); + + nb::enum_(m, "BasisStates", "Enumeration of basis states.") + .value("zero", dd::BasisStates::zero, + R"pb(The computational basis state :math:`|0\rangle`.)pb") + .value("one", dd::BasisStates::one, + R"pb(The computational basis state :math:`|1\rangle`.)pb") + .value( + "plus", dd::BasisStates::plus, + R"pb(The superposition state :math:`|+\rangle = \frac{1}{\sqrt{2}} (|0\rangle + |1\rangle)`.)pb") + .value( + "minus", dd::BasisStates::minus, + R"pb(The superposition state :math:`|-\rangle = \frac{1}{\sqrt{2}} (|0\rangle - |1\rangle)`.)pb") + .value( + "right", dd::BasisStates::right, + R"pb(The superposition state :math:`|R\rangle = \frac{1}{\sqrt{2}} (|0\rangle - i |1\rangle)`.)pb") + .value( + "left", dd::BasisStates::left, + R"pb(The superposition state :math:`|L\rangle = \frac{1}{\sqrt{2}} (|0\rangle + i |1\rangle)`.)pb"); dd.def( "basis_state", [](dd::Package& p, const size_t numQubits, const std::vector& state) { - return dd::makeBasisState(numQubits, state, p); + return makeBasisState(numQubits, state, p); }, "num_qubits"_a, "state"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + R"pb(Create the DD for the basis state :math:`| B_{n - 1} \ldots B_0 \rangle`, where :math:`B_i \in \{0, 1, +\, -\, L, R\}`. + +Args: + num_qubits: The number of qubits. + Must not be greater than the number of qubits the DDPackage is configured with. + state: The state as an iterable of :class:`BasisStates`. + Must be at least `num_qubits` long. + +Returns: + The DD for the basis state. + The resulting state is guaranteed to have its reference count increased.)pb"); dd.def( "ghz_state", [](dd::Package& p, const size_t numQubits) { - return dd::makeGHZState(numQubits, p); + return makeGHZState(numQubits, p); }, "num_qubits"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + R"pb(Create the DD for the GHZ state :math:`\frac{1}{\sqrt{2}} (| 0 \ldots 0 \rangle + |1 \ldots 1 \rangle)`. + +Args: + num_qubits: The number of qubits. + Must not be greater than the number of qubits the DDPackage is configured with. + +Returns: + The DD for the GHZ state. + The resulting state is guaranteed to have its reference count increased.)pb"); dd.def( "w_state", [](dd::Package& p, const size_t numQubits) { - return dd::makeWState(numQubits, p); + return makeWState(numQubits, p); }, "num_qubits"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + R"pb(Create the DD for the W state :math:`|W\rangle`. + +.. math:: + |W\rangle = \frac{1}{\sqrt{n}} (| 100 \ldots 0 \rangle + | 010 \ldots 0 \rangle + \ldots + | 000 \ldots 1 \rangle) + +Args: + num_qubits: The number of qubits. + Must not be greater than the number of qubits the DDPackage is configured with. + +Returns: + The DD for the W state. + The resulting state is guaranteed to have its reference count increased.)pb"); dd.def( "from_vector", - [](dd::Package& p, const py::array_t>& v) { - const auto data = v.unchecked<1>(); - const auto length = static_cast(data.shape(0)); + [](dd::Package& p, const Vector& v) { + const auto length = v.shape(0); if (length == 0) { return dd::vEdge::one(); } @@ -183,30 +272,50 @@ void registerDDPackage(const py::module& mod) { "State vector must have a length of a power of two."); } if (length == 1) { - const auto state = dd::vEdge::terminal(p.cn.lookup(data(0))); + const auto state = dd::vEdge::terminal(p.cn.lookup(v(0))); p.incRef(state); return state; } - const auto level = static_cast(std::log2(length) - 1); - const auto state = makeDDFromVector(p, data, 0, length, level); - const dd::vEdge e{state.p, p.cn.lookup(state.w)}; + const auto state = makeDDFromVector(p, v, 0, length, level); + const dd::vEdge e{.p = state.p, .w = p.cn.lookup(state.w)}; p.incRef(e); return e; }, "state"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Create a DD from a state vector. + +Args: + state: The state vector. + Must have a length that is a power of 2. + Must not require more qubits than the DDPackage is configured with. + +Returns: + The DD for the vector. + The resulting state is guaranteed to have its reference count increased.)pb"); dd.def( "apply_unitary_operation", [](dd::Package& p, const dd::vEdge& v, const qc::Operation& op, const qc::Permutation& perm = {}) { - return dd::applyUnitaryOperation(op, v, p, perm); + return applyUnitaryOperation(op, v, p, perm); }, "vec"_a, "operation"_a, "permutation"_a = qc::Permutation{}, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Apply a unitary operation to the DD. + +Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + +Args: + vec: The input DD. + operation: The operation. Must be unitary. + permutation: The permutation of the qubits. Defaults to the identity permutation. + +Returns: + The resulting DD.)pb"); dd.def( "apply_measurement", @@ -216,36 +325,75 @@ void registerDDPackage(const py::module& mod) { static std::mt19937_64 rng(std::random_device{}()); auto measurementsCopy = measurements; return std::pair{ - dd::applyMeasurement(op, v, p, rng, measurementsCopy, perm), + applyMeasurement(op, v, p, rng, measurementsCopy, perm), measurementsCopy}; }, "vec"_a, "operation"_a, "measurements"_a, "permutation"_a = qc::Permutation{}, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Apply a measurement to the DD. + +Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count + +Args: + vec: The input DD. + operation: The measurement operation. + measurements: A list of bits with existing measurement outcomes. + permutation: The permutation of the qubits. Defaults to the identity permutation. + +Returns: + The resulting DD after the measurement as well as the updated measurement outcomes.)pb"); dd.def( "apply_reset", [](dd::Package& p, const dd::vEdge& v, const qc::NonUnitaryOperation& op, const qc::Permutation& perm = {}) { static std::mt19937_64 rng(std::random_device{}()); - return dd::applyReset(op, v, p, rng, perm); + return applyReset(op, v, p, rng, perm); }, "vec"_a, "operation"_a, "permutation"_a = qc::Permutation{}, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Apply a reset to the DD. + +Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + +Args: + vec: The input DD. + operation: The reset operation. + permutation: The permutation of the qubits. Defaults to the identity permutation. + +Returns: + The resulting DD after the reset.)pb"); dd.def( "apply_if_else_operation", [](dd::Package& p, const dd::vEdge& v, const qc::IfElseOperation& op, const std::vector& measurements, const qc::Permutation& perm = {}) { - return dd::applyIfElseOperation(op, v, p, measurements, perm); + return applyIfElseOperation(op, v, p, measurements, perm); }, "vec"_a, "operation"_a, "measurements"_a, "permutation"_a = qc::Permutation{}, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + R"pb(Apply a classically controlled operation to the DD. + +Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + +Args: + vec: The input DD. + operation: The classically controlled operation. + measurements: A list of bits with stored measurement outcomes. + permutation: The permutation of the qubits. Defaults to the identity permutation. + +Returns: + The resulting DD after the operation.)pb"); dd.def( "measure_collapsing", @@ -253,7 +401,18 @@ void registerDDPackage(const py::module& mod) { static std::mt19937_64 rng(std::random_device{}()); return p.measureOneCollapsing(v, q, rng); }, - "vec"_a, "qubit"_a); + "vec"_a, "qubit"_a, R"pb(Measure a qubit and collapse the DD. + +Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + +Args: + vec: The input DD. + qubit: The qubit to measure. + +Returns: + The measurement outcome.)pb"); dd.def( "measure_all", @@ -261,119 +420,184 @@ void registerDDPackage(const py::module& mod) { static std::mt19937_64 rng(std::random_device{}()); return p.measureAll(v, collapse, rng); }, - "vec"_a, "collapse"_a = false); + "vec"_a, "collapse"_a = false, R"pb(Measure all qubits. + +Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + +Args: + vec: The input DD. + collapse: Whether to collapse the DD. + +Returns: + The measurement outcome.)pb"); + + dd.def_static("identity", &dd::Package::makeIdent, + R"pb(Create the DD for the identity matrix :math:`I`. - dd.def_static("identity", &dd::Package::makeIdent); +Notes: + Returns the global one-terminal (identity matrix), which is package-agnostic and safe to use across DDPackage instances. + +Returns: + The DD for the identity matrix.)pb"); - using NumPyMatrix = py::array_t, - py::array::c_style | py::array::forcecast>; dd.def( "single_qubit_gate", - [](dd::Package& p, const NumPyMatrix& m, const dd::Qubit target) { - if (m.ndim() != 2 || m.shape(0) != 2 || m.shape(1) != 2) { - throw std::invalid_argument("Matrix must be 2x2."); - } - const auto data = m.unchecked<2>(); - return p.makeGateDD({data(0, 0), data(0, 1), data(1, 0), data(1, 1)}, + [](dd::Package& p, const SingleQubitMatrix& mat, const dd::Qubit target) { + return p.makeGateDD({mat(0, 0), mat(0, 1), mat(1, 0), mat(1, 1)}, target); }, "matrix"_a, "target"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Create the DD for a single-qubit gate. + +Args: + matrix: The :math:`2 \times 2` matrix representing the single-qubit gate. + target: The target qubit. + +Returns: + The DD for the single-qubit gate.)pb"); dd.def( "controlled_single_qubit_gate", - [](dd::Package& p, const NumPyMatrix& m, const qc::Control& control, - const dd::Qubit target) { - if (m.ndim() != 2 || m.shape(0) != 2 || m.shape(1) != 2) { - throw std::invalid_argument("Matrix must be 2x2."); - } - const auto data = m.unchecked<2>(); - return p.makeGateDD({data(0, 0), data(0, 1), data(1, 0), data(1, 1)}, + [](dd::Package& p, const SingleQubitMatrix& mat, + const qc::Control& control, const dd::Qubit target) { + return p.makeGateDD({mat(0, 0), mat(0, 1), mat(1, 0), mat(1, 1)}, control, target); }, "matrix"_a, "control"_a, "target"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + nb::sig( + "def controlled_single_qubit_gate(self, " + "matrix: Annotated[NDArray[numpy.complex128], {\"shape\": (2, 2)}]," + "control: mqt.core.ir.operations.Control | int," + "target: int) -> mqt.core.dd.MatrixDD"), + R"pb(Create the DD for a controlled single-qubit gate. + +Args: + matrix: The :math:`2 \times 2` matrix representing the single-qubit gate. + control: The control qubit. + target: The target qubit. + +Returns: + The DD for the controlled single-qubit gate.)pb"); dd.def( "multi_controlled_single_qubit_gate", - [](dd::Package& p, const NumPyMatrix& m, const qc::Controls& controls, - const dd::Qubit target) { - if (m.ndim() != 2 || m.shape(0) != 2 || m.shape(1) != 2) { - throw std::invalid_argument("Matrix must be 2x2."); - } - const auto data = m.unchecked<2>(); - return p.makeGateDD({data(0, 0), data(0, 1), data(1, 0), data(1, 1)}, + [](dd::Package& p, const SingleQubitMatrix& mat, + const qc::Controls& controls, const dd::Qubit target) { + return p.makeGateDD({mat(0, 0), mat(0, 1), mat(1, 0), mat(1, 1)}, controls, target); }, "matrix"_a, "controls"_a, "target"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + nb::sig( + "def multi_controlled_single_qubit_gate(self, " + "matrix: Annotated[NDArray[numpy.complex128], {\"shape\": (2, 2)}]," + "controls: collections.abc.Set[mqt.core.ir.operations.Control | int]," + "target: int) -> mqt.core.dd.MatrixDD"), + R"pb(Create the DD for a multi-controlled single-qubit gate. + +Args: + matrix: The :math:`2 \times 2` matrix representing the single-qubit gate. + controls: The control qubits. + target: The target qubit. + +Returns: + The DD for the multi-controlled single-qubit gate.)pb"); dd.def( "two_qubit_gate", - [](dd::Package& p, const NumPyMatrix& m, const dd::Qubit target0, + [](dd::Package& p, const TwoQubitMatrix& mat, const dd::Qubit target0, const dd::Qubit target1) { - if (m.ndim() != 2 || m.shape(0) != 4 || m.shape(1) != 4) { - throw std::invalid_argument("Matrix must be 4x4."); - } - const auto data = m.unchecked<2>(); return p.makeTwoQubitGateDD( - {std::array{data(0, 0), data(0, 1), data(0, 2), data(0, 3)}, - {data(1, 0), data(1, 1), data(1, 2), data(1, 3)}, - {data(2, 0), data(2, 1), data(2, 2), data(2, 3)}, - {data(3, 0), data(3, 1), data(3, 2), data(3, 3)}}, + {std::array{mat(0, 0), mat(0, 1), mat(0, 2), mat(0, 3)}, + {mat(1, 0), mat(1, 1), mat(1, 2), mat(1, 3)}, + {mat(2, 0), mat(2, 1), mat(2, 2), mat(2, 3)}, + {mat(3, 0), mat(3, 1), mat(3, 2), mat(3, 3)}}, target0, target1); }, "matrix"_a, "target0"_a, "target1"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Create the DD for a two-qubit gate. + +Args: + matrix: The :math:`4 \times 4` matrix representing the two-qubit gate. + target0: The first target qubit. + target1: The second target qubit. + +Returns: + The DD for the two-qubit gate.)pb"); dd.def( "controlled_two_qubit_gate", - [](dd::Package& p, const NumPyMatrix& m, const qc::Control& control, + [](dd::Package& p, const TwoQubitMatrix& mat, const qc::Control& control, const dd::Qubit target0, const dd::Qubit target1) { - if (m.ndim() != 2 || m.shape(0) != 4 || m.shape(1) != 4) { - throw std::invalid_argument("Matrix must be 4x4."); - } - const auto data = m.unchecked<2>(); return p.makeTwoQubitGateDD( - {std::array{data(0, 0), data(0, 1), data(0, 2), data(0, 3)}, - {data(1, 0), data(1, 1), data(1, 2), data(1, 3)}, - {data(2, 0), data(2, 1), data(2, 2), data(2, 3)}, - {data(3, 0), data(3, 1), data(3, 2), data(3, 3)}}, + {std::array{mat(0, 0), mat(0, 1), mat(0, 2), mat(0, 3)}, + {mat(1, 0), mat(1, 1), mat(1, 2), mat(1, 3)}, + {mat(2, 0), mat(2, 1), mat(2, 2), mat(2, 3)}, + {mat(3, 0), mat(3, 1), mat(3, 2), mat(3, 3)}}, control, target0, target1); }, "matrix"_a, "control"_a, "target0"_a, "target1"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + nb::sig( + "def controlled_two_qubit_gate(self, " + "matrix: Annotated[NDArray[numpy.complex128], {\"shape\": (4, 4)}]," + "control: mqt.core.ir.operations.Control | int," + "target0: int, target1: int) -> mqt.core.dd.MatrixDD"), + R"pb(Create the DD for a controlled two-qubit gate. + +Args: + matrix: The :math:`4 \times 4` matrix representing the two-qubit gate. + control: The control qubit. + target0: The first target qubit. + target1: The second target qubit. + +Returns: + The DD for the controlled two-qubit gate.)pb"); dd.def( "multi_controlled_two_qubit_gate", - [](dd::Package& p, const NumPyMatrix& m, const qc::Controls& controls, - const dd::Qubit target0, const dd::Qubit target1) { - if (m.ndim() != 2 || m.shape(0) != 4 || m.shape(1) != 4) { - throw std::invalid_argument("Matrix must be 4x4."); - } - const auto data = m.unchecked<2>(); + [](dd::Package& p, const TwoQubitMatrix& mat, + const qc::Controls& controls, const dd::Qubit target0, + const dd::Qubit target1) { return p.makeTwoQubitGateDD( - {std::array{data(0, 0), data(0, 1), data(0, 2), data(0, 3)}, - {data(1, 0), data(1, 1), data(1, 2), data(1, 3)}, - {data(2, 0), data(2, 1), data(2, 2), data(2, 3)}, - {data(3, 0), data(3, 1), data(3, 2), data(3, 3)}}, + {std::array{mat(0, 0), mat(0, 1), mat(0, 2), mat(0, 3)}, + {mat(1, 0), mat(1, 1), mat(1, 2), mat(1, 3)}, + {mat(2, 0), mat(2, 1), mat(2, 2), mat(2, 3)}, + {mat(3, 0), mat(3, 1), mat(3, 2), mat(3, 3)}}, controls, target0, target1); }, "matrix"_a, "controls"_a, "target0"_a, "target1"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), + nb::sig( + "def multi_controlled_two_qubit_gate(self, " + "matrix: Annotated[NDArray[numpy.complex128], {\"shape\": (4, 4)}]," + "controls: collections.abc.Set[mqt.core.ir.operations.Control | int]," + "target0: int, target1: int) -> mqt.core.dd.MatrixDD"), + R"pb(Create the DD for a multi-controlled two-qubit gate. + +Args: + matrix: The :math:`4 \times 4` matrix representing the two-qubit gate. + controls: The control qubits. + target0: The first target qubit. + target1: The second target qubit. + +Returns: + The DD for the multi-controlled two-qubit gate.)pb"); dd.def( "from_matrix", - [](dd::Package& p, const NumPyMatrix& m) { - const auto data = m.unchecked<2>(); - const auto rows = static_cast(data.shape(0)); - const auto cols = static_cast(data.shape(1)); + [](dd::Package& p, const Matrix& mat) { + const auto rows = mat.shape(0); + const auto cols = mat.shape(1); if (rows != cols) { throw std::invalid_argument("Matrix must be square."); } @@ -385,35 +609,60 @@ void registerDDPackage(const py::module& mod) { "Matrix must have a size of a power of two."); } if (rows == 1) { - return dd::mEdge::terminal(p.cn.lookup(data(0, 0))); + return dd::mEdge::terminal(p.cn.lookup(mat(0, 0))); } const auto level = static_cast(std::log2(rows) - 1); - const auto matrixDD = - makeDDFromMatrix(p, data, 0, rows, 0, cols, level); - return dd::mEdge{matrixDD.p, p.cn.lookup(matrixDD.w)}; + const auto matrixDD = makeDDFromMatrix(p, mat, 0, rows, 0, cols, level); + return dd::mEdge{.p = matrixDD.p, .w = p.cn.lookup(matrixDD.w)}; }, "matrix"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Create a DD from a matrix. + +Args: + matrix: The matrix. Must be square and have a size that is a power of 2. + +Returns: + The DD for the matrix.)pb"); dd.def( "from_operation", [](dd::Package& p, const qc::Operation& op, const bool invert = false) { if (invert) { - return dd::getInverseDD(op, p); + return getInverseDD(op, p); } - return dd::getDD(op, p); + return getDD(op, p); }, "operation"_a, "invert"_a = false, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Create a DD from an operation. + +Args: + operation: The operation. Must be unitary. + invert: Whether to get the inverse of the operation. + +Returns: + The DD for the operation.)pb"); // Reference counting and garbage collection - dd.def("inc_ref_vec", &dd::Package::incRef, "vec"_a); - dd.def("inc_ref_mat", &dd::Package::incRef, "mat"_a); - dd.def("dec_ref_vec", &dd::Package::decRef, "vec"_a); - dd.def("dec_ref_mat", &dd::Package::decRef, "mat"_a); - dd.def("garbage_collect", &dd::Package::garbageCollect, "force"_a = false); + dd.def("inc_ref_vec", &dd::Package::incRef, "vec"_a, + "Increment the reference count of a vector."); + dd.def("inc_ref_mat", &dd::Package::incRef, "mat"_a, + "Increment the reference count of a matrix."); + dd.def("dec_ref_vec", &dd::Package::decRef, "vec"_a, + "Decrement the reference count of a vector."); + dd.def("dec_ref_mat", &dd::Package::decRef, "mat"_a, + "Decrement the reference count of a matrix."); + dd.def("garbage_collect", &dd::Package::garbageCollect, "force"_a = false, + R"pb(Perform garbage collection on the DDPackage. + +Args: + force: Whether to force garbage collection. + If set to True, garbage collection is performed regardless of the current memory usage. + If set to False, garbage collection is only performed if the memory usage exceeds a certain threshold. + +Returns: + Whether any nodes were collected during garbage collection.)pb"); // Operations on DDs dd.def("vector_add", @@ -421,22 +670,64 @@ void registerDDPackage(const py::module& mod) { const dd::vEdge&, const dd::vEdge&)>(&dd::Package::add), "lhs"_a, "rhs"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Add two vectors. + +Notes: + It is the caller's responsibility to update the reference count of the input and output vectors after the operation. + + Both vectors must have the same number of qubits. + +Args: + lhs: The left vector. + rhs: The right vector. + +Returns: + The sum of the two vectors.)pb"); dd.def("matrix_add", static_cast(&dd::Package::add), "lhs"_a, "rhs"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Add two matrices. + +Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + + Both matrices must have the same number of qubits. + +Args: + lhs: The left matrix. + rhs: The right matrix. + +Returns: + The sum of the two matrices.)pb"); dd.def("conjugate", &dd::Package::conjugate, "vec"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Conjugate a vector. + +Notes: + It is the caller's responsibility to update the reference count of the input and output vectors after the operation. + +Args: + vec: The vector. + +Returns: + The conjugated vector.)pb"); dd.def("conjugate_transpose", &dd::Package::conjugateTranspose, "mat"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Conjugate transpose a matrix. + +Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + +Args: + mat: The matrix. + +Returns: + The conjugate transposed matrix.)pb"); dd.def( "matrix_vector_multiply", @@ -445,7 +736,19 @@ void registerDDPackage(const py::module& mod) { }, "mat"_a, "vec"_a, // keep the DD package alive while the returned vector DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Multiply a matrix with a vector. + +Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + + The vector must have at least as many qubits as the matrix non-trivially acts on. + +Args: + mat: The matrix. + vec: The vector. + +Returns: + The product of the matrix and the vector.)pb"); dd.def( "matrix_multiply", @@ -454,19 +757,62 @@ void registerDDPackage(const py::module& mod) { }, "lhs"_a, "rhs"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Multiply two matrices. + +Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + +Args: + lhs: The left matrix. + rhs: The right matrix. + +Returns: + The product of the two matrices.)pb"); dd.def( "inner_product", [](dd::Package& p, const dd::vEdge& lhs, const dd::vEdge& rhs) { return std::complex{p.innerProduct(lhs, rhs)}; }, - "lhs"_a, "rhs"_a); + "lhs"_a, "rhs"_a, R"pb(Compute the inner product of two vectors. + +Notes: + Both vectors must have the same number of qubits. + +Args: + lhs: The left vector. + rhs: The right vector. + +Returns: + The inner product of the two vectors.)pb"); + + dd.def("fidelity", &dd::Package::fidelity, "lhs"_a, "rhs"_a, + R"pb(Compute the fidelity of two vectors. - dd.def("fidelity", &dd::Package::fidelity, "lhs"_a, "rhs"_a); +Notes: + Both vectors must have the same number of qubits. + +Args: + lhs: The left vector. + rhs: The right vector. + +Returns: + The fidelity of the two vectors.)pb"); dd.def("expectation_value", &dd::Package::expectationValue, "observable"_a, - "state"_a); + "state"_a, R"pb(Compute the expectation value of an observable. + +Notes: + The state must have at least as many qubits as the observable non-trivially acts on. + + The method computes :math:`\langle \psi | O | \psi \rangle` as :math:`\langle \psi | (O | \psi \rangle)`. + +Args: + observable: The observable. + state: The state. + +Returns: + The expectation value of the observable.)pb"); dd.def("vector_kronecker", static_cast()); + nb::keep_alive<0, 1>(), + R"pb(Compute the Kronecker product of two vectors. + +Notes: + It is the caller's responsibility to update the reference count of the input and output vectors after the operation. + +Args: + top: The top vector. + bottom: The bottom vector. + bottom_num_qubits: The number of qubits of the bottom vector. + increment_index: Whether to increment the indexes of the top vector. + +Returns: + The Kronecker product of the two vectors.)pb"); dd.def("matrix_kronecker", static_cast()); + nb::keep_alive<0, 1>(), + R"pb(Compute the Kronecker product of two matrices. + +Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + +Args: + top: The top matrix. + bottom: The bottom matrix. + bottom_num_qubits: The number of qubits of the bottom matrix. + increment_index: Whether to increment the indexes of the top matrix. + +Returns: + The Kronecker product of the two matrices.)pb"); dd.def("partial_trace", &dd::Package::partialTrace, "mat"_a, "eliminate"_a, // keep the DD package alive while the returned matrix DD is alive. - py::keep_alive<0, 1>()); + nb::keep_alive<0, 1>(), R"pb(Compute the partial trace of a matrix. + +Args: + mat: The matrix. + eliminate: The qubits to eliminate. Must be at least as long as the number of qubits of the matrix. + +Returns: + The partial trace of the matrix.)pb"); dd.def( "trace", [](dd::Package& p, const dd::mEdge& mat, const size_t numQubits) { return std::complex{p.trace(mat, numQubits)}; }, - "mat"_a, "num_qubits"_a); + "mat"_a, "num_qubits"_a, R"pb(Compute the trace of a matrix. + +Args: + mat: The matrix. + num_qubits: The number of qubits of the matrix. + +Returns: + The trace of the matrix.)pb"); } + } // namespace mqt diff --git a/bindings/dd/register_matrix_dds.cpp b/bindings/dd/register_matrix_dds.cpp index fe6298596e..d52d188542 100644 --- a/bindings/dd/register_matrix_dds.cpp +++ b/bindings/dd/register_matrix_dds.cpp @@ -13,77 +13,106 @@ #include "dd/Export.hpp" #include "dd/Node.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -#include // NOLINT(misc-include-cleaner) -// clang-format on - #include #include #include +#include +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include -#include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; -struct Matrix { - std::vector> data; - size_t n; -}; +using Matrix = nb::ndarray, nb::ndim<2>>; // NOLINTNEXTLINE(misc-use-internal-linkage) Matrix getMatrix(const dd::mEdge& m, const size_t numQubits, const dd::fp threshold = 0.) { + if (numQubits > 20U) { + throw nb::value_error("num_qubits exceeds practical limit of 20"); + } + if (numQubits == 0U) { - return Matrix{.data = {static_cast>(m.w)}, .n = 1}; + auto dataPtr = std::make_unique>(m.w); + auto* data = dataPtr.release(); + const nb::capsule owner(data, [](void* ptr) noexcept { + delete static_cast*>(ptr); + }); + return Matrix(data, {1, 1}, owner); } - const size_t dim = 1ULL << numQubits; - auto data = std::vector>(dim * dim); + + const auto dim = 1ULL << numQubits; + auto dataPtr = std::make_unique[]>(dim * dim); m.traverseMatrix( std::complex{1., 0.}, 0ULL, 0ULL, - [&data, dim](const std::size_t i, const std::size_t j, - const std::complex& c) { data[(i * dim) + j] = c; }, + [&dataPtr, dim](const std::size_t i, const std::size_t j, + const std::complex& c) { + dataPtr[(i * dim) + j] = c; + }, numQubits, threshold); - return Matrix{.data = data, .n = dim}; + auto* data = dataPtr.release(); + const nb::capsule owner(data, [](void* ptr) noexcept { + delete[] static_cast*>(ptr); + }); + return Matrix(data, {dim, dim}, owner); } // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerMatrixDDs(const py::module& mod) { - auto mat = py::class_(mod, "MatrixDD"); +void registerMatrixDDs(const nb::module_& m) { + auto mat = nb::class_( + m, "MatrixDD", "A class representing a matrix decision diagram (DD)."); + + mat.def("is_terminal", &dd::mEdge::isTerminal, + "Check if the DD is a terminal node."); + mat.def("is_zero_terminal", &dd::mEdge::isZeroTerminal, + "Check if the DD is a zero terminal node."); - mat.def("is_terminal", &dd::mEdge::isTerminal); - mat.def("is_zero_terminal", &dd::mEdge::isZeroTerminal); mat.def("is_identity", &dd::mEdge::isIdentity<>, - "up_to_global_phase"_a = true); + "up_to_global_phase"_a = true, + R"pb(Check if the DD represents the identity matrix. + +Args: + up_to_global_phase: Whether to ignore global phase. - mat.def("size", py::overload_cast<>(&dd::mEdge::size, py::const_)); +Returns: + Whether the DD represents the identity matrix.)pb"); + + mat.def("size", nb::overload_cast<>(&dd::mEdge::size, nb::const_), + "Get the size of the DD by traversing it once."); mat.def("get_entry", &dd::mEdge::getValueByIndex<>, "num_qubits"_a, "row"_a, - "col"_a); + "col"_a, "Get the entry of the matrix by row and column index."); + mat.def("get_entry_by_path", &dd::mEdge::getValueByPath, "num_qubits"_a, - "decisions"_a); + "decisions"_a, R"pb(Get the entry of the matrix by decisions. + +Args: + num_qubits: The number of qubits. + decisions: The decisions as a string of `0`, `1`, `2`, or `3`, where `decisions[i]` corresponds to the successor to follow at level `i` of the DD. + Must be at least `num_qubits` long. + +Returns: + The entry of the matrix.)pb"); - py::class_(mod, "Matrix", py::buffer_protocol()) - .def_buffer([](Matrix& matrix) -> py::buffer_info { - return py::buffer_info( - matrix.data.data(), sizeof(std::complex), - // NOLINTNEXTLINE(misc-include-cleaner) - py::format_descriptor>::format(), 2, - {matrix.n, matrix.n}, - {sizeof(std::complex) * matrix.n, - sizeof(std::complex)}); - }); + mat.def("get_matrix", &getMatrix, "num_qubits"_a, "threshold"_a = 0., + R"pb(Get the matrix represented by the DD. - mat.def("get_matrix", &getMatrix, "num_qubits"_a, "threshold"_a = 0.); +Args: + num_qubits: The number of qubits. + threshold: The threshold for not including entries in the matrix. Defaults to 0.0. + +Returns: + The matrix. + +Raises: + MemoryError: If the memory allocation fails.)pb"); mat.def( "to_dot", @@ -91,11 +120,22 @@ void registerMatrixDDs(const py::module& mod) { const bool edgeLabels = false, const bool classic = false, const bool memory = false, const bool formatAsPolar = true) { std::ostringstream os; - dd::toDot(e, os, colored, edgeLabels, classic, memory, formatAsPolar); + toDot(e, os, colored, edgeLabels, classic, memory, formatAsPolar); return os.str(); }, "colored"_a = true, "edge_labels"_a = false, "classic"_a = false, - "memory"_a = false, "format_as_polar"_a = true); + "memory"_a = false, "format_as_polar"_a = true, + R"pb(Convert the DD to a DOT graph that can be plotted via Graphviz. + +Args: + colored: Whether to use colored edge weights + edge_labels: Whether to include edge weights as labels. + classic: Whether to use the classic DD visualization style. + memory: Whether to include memory information. For debugging purposes only. + format_as_polar: Whether to format the edge weights in polar coordinates. + +Returns: + The DOT graph.)pb"); mat.def( "to_svg", @@ -106,10 +146,21 @@ void registerMatrixDDs(const py::module& mod) { // replace the filename extension with .dot const auto dotFilename = filename.substr(0, filename.find_last_of('.')) + ".dot"; - dd::export2Dot(e, dotFilename, colored, edgeLabels, classic, memory, - true, formatAsPolar); + export2Dot(e, dotFilename, colored, edgeLabels, classic, memory, true, + formatAsPolar); }, "filename"_a, "colored"_a = true, "edge_labels"_a = false, - "classic"_a = false, "memory"_a = false, "format_as_polar"_a = true); + "classic"_a = false, "memory"_a = false, "format_as_polar"_a = true, + R"pb(Convert the DD to an SVG file that can be viewed in a browser. + +Requires the `dot` command from Graphviz to be installed and available in the PATH. + +Args: + filename: The filename of the SVG file. Any file extension will be replaced by `.dot` and then `.svg`. + colored: Whether to use colored edge weights. + edge_labels: Whether to include edge weights as labels. + classic: Whether to use the classic DD visualization style. + memory: Whether to include memory information. For debugging purposes only. + format_as_polar: Whether to format the edge weights in polar coordinates.)pb"); } } // namespace mqt diff --git a/bindings/dd/register_vector_dds.cpp b/bindings/dd/register_vector_dds.cpp index d32465970a..065a682571 100644 --- a/bindings/dd/register_vector_dds.cpp +++ b/bindings/dd/register_vector_dds.cpp @@ -13,51 +13,65 @@ #include "dd/Export.hpp" #include "dd/Node.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -#include // NOLINT(misc-include-cleaner) -// clang-format on - +#include #include #include #include +#include +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; -struct Vector { - dd::CVec v; -}; +using Vector = nb::ndarray, nb::ndim<1>>; // NOLINTNEXTLINE(misc-use-internal-linkage) Vector getVector(const dd::vEdge& v, const dd::fp threshold = 0.) { - return {v.getVector(threshold)}; + auto vec = v.getVector(threshold); + auto dataPtr = std::make_unique[]>(vec.size()); + std::ranges::copy(vec, dataPtr.get()); + auto* data = dataPtr.release(); + const nb::capsule owner(data, [](void* ptr) noexcept { + delete[] static_cast*>(ptr); + }); + return Vector(data, {vec.size()}, owner); } // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerVectorDDs(const py::module& mod) { - auto vec = py::class_(mod, "VectorDD"); +void registerVectorDDs(const nb::module_& m) { + auto vec = nb::class_( + m, "VectorDD", "A class representing a vector decision diagram (DD)."); + + vec.def("is_terminal", &dd::vEdge::isTerminal, + "Check if the DD is a terminal node."); - vec.def("is_terminal", &dd::vEdge::isTerminal); - vec.def("is_zero_terminal", &dd::vEdge::isZeroTerminal); + vec.def("is_zero_terminal", &dd::vEdge::isZeroTerminal, + "Check if the DD is a zero terminal node."); - vec.def("size", py::overload_cast<>(&dd::vEdge::size, py::const_)); + vec.def("size", nb::overload_cast<>(&dd::vEdge::size, nb::const_), + "Get the size of the DD by traversing it once."); vec.def( "__getitem__", - [](const dd::vEdge& v, const size_t idx) { - return v.getValueByIndex(idx); + [](const dd::vEdge& v, nb::ssize_t idx) { + const auto n = static_cast(v.size()); + if (idx < 0) { + idx += n; + } + if (idx < 0 || idx >= n) { + throw nb::index_error(); + } + return v.getValueByIndex(static_cast(idx)); }, - "index"_a); + "key"_a, "Get the amplitude of a basis state by index."); vec.def( "get_amplitude", @@ -65,18 +79,28 @@ void registerVectorDDs(const py::module& mod) { const std::string& decisions) { return v.getValueByPath(numQubits, decisions); }, - "num_qubits"_a, "decisions"_a); + "num_qubits"_a, "decisions"_a, + R"pb(Get the amplitude of a basis state by decisions. + +Args: + num_qubits: The number of qubits. + decisions: The decisions as a string of bits (`0` or `1`), where `decisions[i]` corresponds to the successor to follow at level `i` of the DD. + Must be at least `num_qubits` long. + +Returns: + The amplitude of the basis state.)pb"); - py::class_(mod, "Vector", py::buffer_protocol()) - .def_buffer([](Vector& vector) -> py::buffer_info { - return py::buffer_info( - vector.v.data(), sizeof(std::complex), - // NOLINTNEXTLINE(misc-include-cleaner) - py::format_descriptor>::format(), 1, - {vector.v.size()}, {sizeof(std::complex)}); - }); + vec.def("get_vector", &getVector, "threshold"_a = 0., + R"pb(Get the state vector represented by the DD. - vec.def("get_vector", &getVector, "threshold"_a = 0.); +Args: + threshold: The threshold for not including amplitudes in the state vector. Defaults to 0.0. + +Returns: + The state vector. + +Raises: + MemoryError: If the memory allocation fails.)pb"); vec.def( "to_dot", @@ -84,11 +108,22 @@ void registerVectorDDs(const py::module& mod) { const bool edgeLabels = false, const bool classic = false, const bool memory = false, const bool formatAsPolar = true) { std::ostringstream os; - dd::toDot(e, os, colored, edgeLabels, classic, memory, formatAsPolar); + toDot(e, os, colored, edgeLabels, classic, memory, formatAsPolar); return os.str(); }, "colored"_a = true, "edge_labels"_a = false, "classic"_a = false, - "memory"_a = false, "format_as_polar"_a = true); + "memory"_a = false, "format_as_polar"_a = true, + R"pb(Convert the DD to a DOT graph that can be plotted via Graphviz. + +Args: + colored: Whether to use colored edge weights + edge_labels: Whether to include edge weights as labels. + classic: Whether to use the classic DD visualization style. + memory: Whether to include memory information. For debugging purposes only. + format_as_polar: Whether to format the edge weights in polar coordinates. + +Returns: + The DOT graph.)pb"); vec.def( "to_svg", @@ -99,11 +134,22 @@ void registerVectorDDs(const py::module& mod) { // replace the filename extension with .dot const auto dotFilename = filename.substr(0, filename.find_last_of('.')) + ".dot"; - dd::export2Dot(e, dotFilename, colored, edgeLabels, classic, memory, - true, formatAsPolar); + export2Dot(e, dotFilename, colored, edgeLabels, classic, memory, true, + formatAsPolar); }, "filename"_a, "colored"_a = true, "edge_labels"_a = false, - "classic"_a = false, "memory"_a = false, "format_as_polar"_a = true); + "classic"_a = false, "memory"_a = false, "format_as_polar"_a = true, + R"pb(Convert the DD to an SVG file that can be viewed in a browser. + +Requires the `dot` command from Graphviz to be installed and available in the PATH. + +Args: + filename: The filename of the SVG file. Any file extension will be replaced by `.dot` and then `.svg`. + colored: Whether to use colored edge weights. + edge_labels: Whether to include edge weights as labels. + classic: Whether to use the classic DD visualization style. + memory: Whether to include memory information. For debugging purposes only. + format_as_polar: Whether to format the edge weights in polar coordinates.)pb"); } } // namespace mqt diff --git a/bindings/fomac/CMakeLists.txt b/bindings/fomac/CMakeLists.txt index 76e491345a..8c5eab1464 100644 --- a/bindings/fomac/CMakeLists.txt +++ b/bindings/fomac/CMakeLists.txt @@ -13,7 +13,7 @@ if(NOT TARGET ${TARGET_NAME}) file(GLOB_RECURSE SOURCES **.cpp) # declare the Python module - add_mqt_python_binding( + add_mqt_python_binding_nanobind( CORE ${TARGET_NAME} ${SOURCES} @@ -23,4 +23,12 @@ if(NOT TARGET ${TARGET_NAME}) . LINK_LIBS MQT::CoreFoMaC) + + # install the Python stub file in editable mode for better IDE support + if(SKBUILD_STATE STREQUAL "editable") + install( + FILES ${PROJECT_SOURCE_DIR}/python/mqt/core/fomac.pyi + DESTINATION . + COMPONENT ${MQT_CORE_TARGET_NAME}_Python) + endif() endif() diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index df98373c17..3f1b00d1cc 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -12,97 +12,180 @@ #include "qdmi/Driver.hpp" +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include -#include -#include // NOLINT(misc-include-cleaner) -#include -#include -#include -#include // NOLINT(misc-include-cleaner) #include #include +#include #include namespace mqt { -namespace py = pybind11; -using namespace py::literals; +namespace nb = nanobind; +using namespace nb::literals; -PYBIND11_MODULE(MQT_CORE_MODULE_NAME, m, py::mod_gil_not_used()) { - auto session = py::class_(m, "Session"); +NB_MODULE(MQT_CORE_MODULE_NAME, m) { + // Session class + auto session = nb::class_( + m, "Session", R"pb(A FoMaC session for managing QDMI devices. + +Allows creating isolated sessions with independent authentication settings. +All authentication parameters are optional and can be provided as keyword arguments to the constructor.)pb"); session.def( - py::init([](const std::optional& token = std::nullopt, - const std::optional& authFile = std::nullopt, - const std::optional& authUrl = std::nullopt, - const std::optional& username = std::nullopt, - const std::optional& password = std::nullopt, - const std::optional& projectId = std::nullopt, - const std::optional& custom1 = std::nullopt, - const std::optional& custom2 = std::nullopt, - const std::optional& custom3 = std::nullopt, - const std::optional& custom4 = std::nullopt, - const std::optional& custom5 = - std::nullopt) -> fomac::Session { - const fomac::SessionConfig config{.token = token, - .authFile = authFile, - .authUrl = authUrl, - .username = username, - .password = password, - .projectId = projectId, - .custom1 = custom1, - .custom2 = custom2, - .custom3 = custom3, - .custom4 = custom4, - .custom5 = custom5}; - return fomac::Session{config}; - }), - "token"_a = std::nullopt, "auth_file"_a = std::nullopt, + "__init__", + [](fomac::Session* self, std::optional token, + std::optional authFile, + std::optional authUrl, + std::optional username, + std::optional password, + std::optional projectId, + std::optional custom1, std::optional custom2, + std::optional custom3, std::optional custom4, + std::optional custom5) { + const fomac::SessionConfig config{.token = std::move(token), + .authFile = std::move(authFile), + .authUrl = std::move(authUrl), + .username = std::move(username), + .password = std::move(password), + .projectId = std::move(projectId), + .custom1 = std::move(custom1), + .custom2 = std::move(custom2), + .custom3 = std::move(custom3), + .custom4 = std::move(custom4), + .custom5 = std::move(custom5)}; + new (self) fomac::Session(config); + }, + nb::kw_only(), "token"_a = std::nullopt, "auth_file"_a = std::nullopt, "auth_url"_a = std::nullopt, "username"_a = std::nullopt, "password"_a = std::nullopt, "project_id"_a = std::nullopt, "custom1"_a = std::nullopt, "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, "custom4"_a = std::nullopt, - "custom5"_a = std::nullopt); + "custom5"_a = std::nullopt, + R"pb(Create a new FoMaC session with optional authentication. + +Args: + token: Authentication token + auth_file: Path to file containing authentication information + auth_url: URL to authentication server + username: Username for authentication + password: Password for authentication + project_id: Project ID for session + custom1: Custom configuration parameter 1 + custom2: Custom configuration parameter 2 + custom3: Custom configuration parameter 3 + custom4: Custom configuration parameter 4 + custom5: Custom configuration parameter 5 + +Raises: + RuntimeError: If auth_file does not exist + RuntimeError: If auth_url has invalid format + +Example: + >>> from mqt.core.fomac import Session + >>> # Session without authentication + >>> session = Session() + >>> devices = session.get_devices() + >>> + >>> # Session with token authentication + >>> session = Session(token="my_secret_token") + >>> devices = session.get_devices() + >>> + >>> # Session with file-based authentication + >>> session = Session(auth_file="/path/to/auth.json") + >>> devices = session.get_devices() + >>> + >>> # Session with multiple parameters + >>> session = Session( + ... auth_url="https://auth.example.com", username="user", password="pass", project_id="project-123" + ... ) + >>> devices = session.get_devices())pb"); + + session.def("get_devices", &fomac::Session::getDevices, + nb::rv_policy::reference_internal, + R"pb(Get available devices from this session. - session.def("get_devices", &fomac::Session::getDevices); +Returns: + List of available devices.)pb"); // Job class - auto job = py::class_(m, "Job"); - job.def("check", &fomac::Session::Job::check); - job.def("wait", &fomac::Session::Job::wait, "timeout"_a = 0); - job.def("cancel", &fomac::Session::Job::cancel); - job.def("get_shots", &fomac::Session::Job::getShots); - job.def("get_counts", &fomac::Session::Job::getCounts); - job.def("get_dense_statevector", &fomac::Session::Job::getDenseStateVector); + auto job = nb::class_( + m, "Job", "A job represents a submitted quantum program execution."); + + job.def("check", &fomac::Session::Job::check, + "Returns the current status of the job."); + + job.def("wait", &fomac::Session::Job::wait, "timeout"_a = 0, + R"pb(Waits for the job to complete. + +Args: + timeout: The maximum time to wait in seconds. If 0, waits indefinitely. + +Returns: + True if the job completed within the timeout, False otherwise.)pb"); + + job.def("cancel", &fomac::Session::Job::cancel, "Cancels the job."); + + job.def("get_shots", &fomac::Session::Job::getShots, + "Returns the raw shot results from the job."); + + job.def("get_counts", &fomac::Session::Job::getCounts, + "Returns the measurement counts from the job."); + + job.def("get_dense_statevector", &fomac::Session::Job::getDenseStateVector, + "Returns the dense statevector from the job (typically only " + "available from simulator devices)."); + job.def("get_dense_probabilities", - &fomac::Session::Job::getDenseProbabilities); - job.def("get_sparse_statevector", &fomac::Session::Job::getSparseStateVector); + &fomac::Session::Job::getDenseProbabilities, + "Returns the dense probabilities from the job (typically only " + "available from simulator devices)."); + + job.def("get_sparse_statevector", &fomac::Session::Job::getSparseStateVector, + "Returns the sparse statevector from the job (typically only " + "available from simulator devices)."); + job.def("get_sparse_probabilities", - &fomac::Session::Job::getSparseProbabilities); - job.def_property_readonly("id", &fomac::Session::Job::getId); - job.def_property_readonly("program_format", - &fomac::Session::Job::getProgramFormat); - job.def_property_readonly("program", &fomac::Session::Job::getProgram); - job.def_property_readonly("num_shots", &fomac::Session::Job::getNumShots); - job.def(py::self == py::self); // NOLINT(misc-redundant-expression) - job.def(py::self != py::self); // NOLINT(misc-redundant-expression) + &fomac::Session::Job::getSparseProbabilities, + "Returns the sparse probabilities from the job (typically only " + "available from simulator devices)."); + + job.def_prop_ro("id", &fomac::Session::Job::getId, "Returns the job ID."); + + job.def_prop_ro("program_format", &fomac::Session::Job::getProgramFormat, + "Returns the program format used for the job."); + + job.def_prop_ro("program", &fomac::Session::Job::getProgram, + "Returns the quantum program submitted for the job."); + + job.def_prop_ro("num_shots", &fomac::Session::Job::getNumShots, + "Returns the number of shots for the job."); + + job.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + job.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); // JobStatus enum - py::native_enum(job, "Status", "enum.Enum", - "Enumeration of job status.") + nb::enum_(job, "Status", "Enumeration of job status.") .value("CREATED", QDMI_JOB_STATUS_CREATED) .value("SUBMITTED", QDMI_JOB_STATUS_SUBMITTED) .value("QUEUED", QDMI_JOB_STATUS_QUEUED) .value("RUNNING", QDMI_JOB_STATUS_RUNNING) .value("DONE", QDMI_JOB_STATUS_DONE) .value("CANCELED", QDMI_JOB_STATUS_CANCELED) - .value("FAILED", QDMI_JOB_STATUS_FAILED) - .export_values() - .finalize(); + .value("FAILED", QDMI_JOB_STATUS_FAILED); // ProgramFormat enum - py::native_enum(m, "ProgramFormat", "enum.Enum", - "Enumeration of program formats.") + nb::enum_(m, "ProgramFormat", + "Enumeration of program formats.") .value("QASM2", QDMI_PROGRAM_FORMAT_QASM2) .value("QASM3", QDMI_PROGRAM_FORMAT_QASM3) .value("QIR_BASE_STRING", QDMI_PROGRAM_FORMAT_QIRBASESTRING) @@ -116,113 +199,226 @@ PYBIND11_MODULE(MQT_CORE_MODULE_NAME, m, py::mod_gil_not_used()) { .value("CUSTOM2", QDMI_PROGRAM_FORMAT_CUSTOM2) .value("CUSTOM3", QDMI_PROGRAM_FORMAT_CUSTOM3) .value("CUSTOM4", QDMI_PROGRAM_FORMAT_CUSTOM4) - .value("CUSTOM5", QDMI_PROGRAM_FORMAT_CUSTOM5) - .export_values() - .finalize(); - - auto device = py::class_(m, "Device"); - - py::native_enum(device, "Status", "enum.Enum", - "Enumeration of device status.") - .value("offline", QDMI_DEVICE_STATUS_OFFLINE) - .value("idle", QDMI_DEVICE_STATUS_IDLE) - .value("busy", QDMI_DEVICE_STATUS_BUSY) - .value("error", QDMI_DEVICE_STATUS_ERROR) - .value("maintenance", QDMI_DEVICE_STATUS_MAINTENANCE) - .value("calibration", QDMI_DEVICE_STATUS_CALIBRATION) - .export_values() - .finalize(); - - auto site = py::class_(device, "Site"); - site.def("index", &fomac::Session::Device::Site::getIndex); - site.def("t1", &fomac::Session::Device::Site::getT1); - site.def("t2", &fomac::Session::Device::Site::getT2); - site.def("name", &fomac::Session::Device::Site::getName); - site.def("x_coordinate", &fomac::Session::Device::Site::getXCoordinate); - site.def("y_coordinate", &fomac::Session::Device::Site::getYCoordinate); - site.def("z_coordinate", &fomac::Session::Device::Site::getZCoordinate); - site.def("is_zone", &fomac::Session::Device::Site::isZone); - site.def("x_extent", &fomac::Session::Device::Site::getXExtent); - site.def("y_extent", &fomac::Session::Device::Site::getYExtent); - site.def("z_extent", &fomac::Session::Device::Site::getZExtent); - site.def("module_index", &fomac::Session::Device::Site::getModuleIndex); - site.def("submodule_index", &fomac::Session::Device::Site::getSubmoduleIndex); + .value("CUSTOM5", QDMI_PROGRAM_FORMAT_CUSTOM5); + + // Device class + auto device = nb::class_( + m, "Device", + "A device represents a quantum device with its properties and " + "capabilities."); + + nb::enum_(device, "Status", + "Enumeration of device status.") + .value("OFFLINE", QDMI_DEVICE_STATUS_OFFLINE) + .value("IDLE", QDMI_DEVICE_STATUS_IDLE) + .value("BUSY", QDMI_DEVICE_STATUS_BUSY) + .value("ERROR", QDMI_DEVICE_STATUS_ERROR) + .value("MAINTENANCE", QDMI_DEVICE_STATUS_MAINTENANCE) + .value("CALIBRATION", QDMI_DEVICE_STATUS_CALIBRATION); + + device.def("name", &fomac::Session::Device::getName, + "Returns the name of the device."); + + device.def("version", &fomac::Session::Device::getVersion, + "Returns the version of the device."); + + device.def("status", &fomac::Session::Device::getStatus, + "Returns the current status of the device."); + + device.def("library_version", &fomac::Session::Device::getLibraryVersion, + "Returns the version of the library used to define the device."); + + device.def("qubits_num", &fomac::Session::Device::getQubitsNum, + "Returns the number of qubits available on the device."); + + device.def("sites", &fomac::Session::Device::getSites, + "Returns the list of all sites (zone and regular sites) available " + "on the device."); + + device.def("regular_sites", &fomac::Session::Device::getRegularSites, + "Returns the list of regular sites (without zone sites) available " + "on the device."); + + device.def("zones", &fomac::Session::Device::getZones, + "Returns the list of zone sites (without regular sites) available " + "on the device."); + + device.def("operations", &fomac::Session::Device::getOperations, + "Returns the list of operations supported by the device."); + + device.def("coupling_map", &fomac::Session::Device::getCouplingMap, + "Returns the coupling map of the device as a list of site pairs."); + + device.def("needs_calibration", &fomac::Session::Device::getNeedsCalibration, + "Returns whether the device needs calibration."); + + device.def("length_unit", &fomac::Session::Device::getLengthUnit, + "Returns the unit of length used by the device."); + + device.def("length_scale_factor", + &fomac::Session::Device::getLengthScaleFactor, + "Returns the scale factor for length used by the device."); + + device.def("duration_unit", &fomac::Session::Device::getDurationUnit, + "Returns the unit of duration used by the device."); + + device.def("duration_scale_factor", + &fomac::Session::Device::getDurationScaleFactor, + "Returns the scale factor for duration used by the device."); + + device.def("min_atom_distance", &fomac::Session::Device::getMinAtomDistance, + "Returns the minimum atom distance on the device."); + + device.def("supported_program_formats", + &fomac::Session::Device::getSupportedProgramFormats, + "Returns the list of program formats supported by the device."); + + device.def("submit_job", &fomac::Session::Device::submitJob, "program"_a, + "program_format"_a, "num_shots"_a, + nb::rv_policy::reference_internal, "Submits a job to the device."); + + device.def("__repr__", [](const fomac::Session::Device& dev) { + return ""; + }); + + device.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + device.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); + + // Site class + auto site = nb::class_( + device, "Site", + "A site represents a potential qubit location on a quantum device."); + + site.def("index", &fomac::Session::Device::Site::getIndex, + "Returns the index of the site."); + + site.def("t1", &fomac::Session::Device::Site::getT1, + "Returns the T1 coherence time of the site."); + + site.def("t2", &fomac::Session::Device::Site::getT2, + "Returns the T2 coherence time of the site."); + + site.def("name", &fomac::Session::Device::Site::getName, + "Returns the name of the site."); + + site.def("x_coordinate", &fomac::Session::Device::Site::getXCoordinate, + "Returns the x coordinate of the site."); + + site.def("y_coordinate", &fomac::Session::Device::Site::getYCoordinate, + "Returns the y coordinate of the site."); + + site.def("z_coordinate", &fomac::Session::Device::Site::getZCoordinate, + "Returns the z coordinate of the site."); + + site.def("is_zone", &fomac::Session::Device::Site::isZone, + "Returns whether the site is a zone."); + + site.def("x_extent", &fomac::Session::Device::Site::getXExtent, + "Returns the x extent of the site."); + + site.def("y_extent", &fomac::Session::Device::Site::getYExtent, + "Returns the y extent of the site."); + + site.def("z_extent", &fomac::Session::Device::Site::getZExtent, + "Returns the z extent of the site."); + + site.def("module_index", &fomac::Session::Device::Site::getModuleIndex, + "Returns the index of the module the site belongs to."); + + site.def("submodule_index", &fomac::Session::Device::Site::getSubmoduleIndex, + "Returns the index of the submodule the site belongs to."); + site.def("__repr__", [](const fomac::Session::Device::Site& s) { return ""; }); - site.def(py::self == py::self); // NOLINT(misc-redundant-expression) - site.def(py::self != py::self); // NOLINT(misc-redundant-expression) - auto operation = - py::class_(device, "Operation"); + site.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + site.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); + + // Operation class + auto operation = nb::class_( + device, "Operation", + "An operation represents a quantum operation that can be performed on a " + "quantum device."); + operation.def("name", &fomac::Session::Device::Operation::getName, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); + "sites"_a.sig("...") = + std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the name of the operation."); + operation.def("qubits_num", &fomac::Session::Device::Operation::getQubitsNum, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); - operation.def("parameters_num", - &fomac::Session::Device::Operation::getParametersNum, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); + "sites"_a.sig("...") = + std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the number of qubits the operation acts on."); + + operation.def( + "parameters_num", &fomac::Session::Device::Operation::getParametersNum, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the number of parameters the operation has."); + operation.def("duration", &fomac::Session::Device::Operation::getDuration, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); + "sites"_a.sig("...") = + std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the duration of the operation."); + operation.def("fidelity", &fomac::Session::Device::Operation::getFidelity, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); + "sites"_a.sig("...") = + std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the fidelity of the operation."); + operation.def("interaction_radius", &fomac::Session::Device::Operation::getInteractionRadius, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); - operation.def("blocking_radius", - &fomac::Session::Device::Operation::getBlockingRadius, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); - operation.def("idling_fidelity", - &fomac::Session::Device::Operation::getIdlingFidelity, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); - operation.def("is_zoned", &fomac::Session::Device::Operation::isZoned); - operation.def("sites", &fomac::Session::Device::Operation::getSites); - operation.def("site_pairs", &fomac::Session::Device::Operation::getSitePairs); + "sites"_a.sig("...") = + std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the interaction radius of the operation."); + + operation.def( + "blocking_radius", &fomac::Session::Device::Operation::getBlockingRadius, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the blocking radius of the operation."); + + operation.def( + "idling_fidelity", &fomac::Session::Device::Operation::getIdlingFidelity, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the idling fidelity of the operation."); + + operation.def("is_zoned", &fomac::Session::Device::Operation::isZoned, + "Returns whether the operation is zoned."); + + operation.def("sites", &fomac::Session::Device::Operation::getSites, + "Returns the list of sites the operation can be performed on."); + + operation.def("site_pairs", &fomac::Session::Device::Operation::getSitePairs, + "Returns the list of site pairs the local 2-qubit operation " + "can be performed on."); + operation.def("mean_shuttling_speed", &fomac::Session::Device::Operation::getMeanShuttlingSpeed, - "sites"_a = std::vector{}, - "params"_a = std::vector{}); + "sites"_a.sig("...") = + std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the mean shuttling speed of the operation."); + operation.def("__repr__", [](const fomac::Session::Device::Operation& op) { return ""; }); - operation.def(py::self == py::self); // NOLINT(misc-redundant-expression) - operation.def(py::self != py::self); // NOLINT(misc-redundant-expression) - - device.def("name", &fomac::Session::Device::getName); - device.def("version", &fomac::Session::Device::getVersion); - device.def("status", &fomac::Session::Device::getStatus); - device.def("library_version", &fomac::Session::Device::getLibraryVersion); - device.def("qubits_num", &fomac::Session::Device::getQubitsNum); - device.def("sites", &fomac::Session::Device::getSites); - device.def("regular_sites", &fomac::Session::Device::getRegularSites); - device.def("zones", &fomac::Session::Device::getZones); - device.def("operations", &fomac::Session::Device::getOperations); - device.def("coupling_map", &fomac::Session::Device::getCouplingMap); - device.def("needs_calibration", &fomac::Session::Device::getNeedsCalibration); - device.def("length_unit", &fomac::Session::Device::getLengthUnit); - device.def("length_scale_factor", - &fomac::Session::Device::getLengthScaleFactor); - device.def("duration_unit", &fomac::Session::Device::getDurationUnit); - device.def("duration_scale_factor", - &fomac::Session::Device::getDurationScaleFactor); - device.def("min_atom_distance", &fomac::Session::Device::getMinAtomDistance); - device.def("supported_program_formats", - &fomac::Session::Device::getSupportedProgramFormats); - device.def("submit_job", &fomac::Session::Device::submitJob, "program"_a, - "program_format"_a, "num_shots"_a); - device.def("__repr__", [](const fomac::Session::Device& dev) { - return ""; - }); - device.def(py::self == py::self); // NOLINT(misc-redundant-expression) - device.def(py::self != py::self); // NOLINT(misc-redundant-expression) + + operation.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + operation.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); #ifndef _WIN32 // Module-level function to add dynamic device libraries on non-Windows @@ -257,12 +453,52 @@ PYBIND11_MODULE(MQT_CORE_MODULE_NAME, m, py::mod_gil_not_used()) { libraryPath, prefix, config); return fomac::Session::Device::fromQDMIDevice(qdmiDevice); }, - "library_path"_a, "prefix"_a, "base_url"_a = std::nullopt, + "library_path"_a, "prefix"_a, nb::kw_only(), "base_url"_a = std::nullopt, "token"_a = std::nullopt, "auth_file"_a = std::nullopt, "auth_url"_a = std::nullopt, "username"_a = std::nullopt, "password"_a = std::nullopt, "custom1"_a = std::nullopt, "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, - "custom4"_a = std::nullopt, "custom5"_a = std::nullopt); + "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, + R"pb(Load a dynamic device library into the QDMI driver. + +This function loads a shared library (.so, .dll, or .dylib) that implements a QDMI device interface and makes it available for use in sessions. + +Note: + This function is only available on non-Windows platforms. + +Args: + library_path: Path to the shared library file to load. + prefix: Function prefix used by the library (e.g., "MY_DEVICE"). + base_url: Optional base URL for the device API endpoint. + token: Optional authentication token. + auth_file: Optional path to authentication file. + auth_url: Optional authentication server URL. + username: Optional username for authentication. + password: Optional password for authentication. + custom1: Optional custom configuration parameter 1. + custom2: Optional custom configuration parameter 2. + custom3: Optional custom configuration parameter 3. + custom4: Optional custom configuration parameter 4. + custom5: Optional custom configuration parameter 5. + +Returns: + Device: The newly loaded device that can be used to create backends. + +Raises: + RuntimeError: If library loading fails or configuration is invalid. + +Examples: + Load a device library with configuration: + + >>> import mqt.core.fomac as fomac + >>> device = fomac.add_dynamic_device_library( + ... "/path/to/libmy_device.so", "MY_DEVICE", base_url="http://localhost:8080", custom1="API_V2" + ... ) + + Now the device can be used directly: + + >>> from mqt.core.plugins.qiskit import QDMIBackend + >>> backend = QDMIBackend(device=device))pb"); #endif // _WIN32 } diff --git a/bindings/ir/CMakeLists.txt b/bindings/ir/CMakeLists.txt index 719c8b6734..b560e19675 100644 --- a/bindings/ir/CMakeLists.txt +++ b/bindings/ir/CMakeLists.txt @@ -11,7 +11,7 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}-ir-bindings) file(GLOB_RECURSE IR_SOURCES **.cpp) # declare the Python module - add_mqt_python_binding( + add_mqt_python_binding_nanobind( CORE ${MQT_CORE_TARGET_NAME}-ir-bindings ${IR_SOURCES} diff --git a/bindings/ir/operations/register_compound_operation.cpp b/bindings/ir/operations/register_compound_operation.cpp index 811b6ac12f..cfaaa5aa34 100644 --- a/bindings/ir/operations/register_compound_operation.cpp +++ b/bindings/ir/operations/register_compound_operation.cpp @@ -11,18 +11,16 @@ #include "ir/operations/CompoundOperation.hpp" #include "ir/operations/Operation.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -// clang-format on - +#include +#include #include #include +#include #include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -30,122 +28,176 @@ namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; using DiffType = std::vector>::difference_type; using SizeType = std::vector>::size_type; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerCompoundOperation(const py::module& m) { +void registerCompoundOperation(const nb::module_& m) { auto wrap = [](DiffType i, const SizeType size) { if (i < 0) { i += static_cast(size); } if (i < 0 || std::cmp_greater_equal(i, size)) { - throw py::index_error(); + throw nb::index_error(); } return i; }; - py::class_(m, "CompoundOperation") - .def(py::init<>()) - .def(py::init([](const std::vector& ops) { - std::vector> uniqueOps; - uniqueOps.reserve(ops.size()); - for (const auto& op : ops) { - uniqueOps.emplace_back(op->clone()); - } - return qc::CompoundOperation(std::move(uniqueOps)); - }), - "ops"_a) - .def("__len__", &qc::CompoundOperation::size) + nb::class_( + m, "CompoundOperation", + nb::sig( + "class CompoundOperation(mqt.core.ir.operations.Operation, " + "collections.abc.MutableSequence[mqt.core.ir.operations.Operation])"), + R"pb(Compound quantum operation. + +This class is used to aggregate and group multiple operations into a single object. +This is useful for optimizations and for representing complex quantum functionality. +A :class:`CompoundOperation` can contain any number of operations, including other :class:`CompoundOperation`'s. + +Args: + ops: The operations that are part of the compound operation.)pb") + + .def(nb::init<>()) + .def( + "__init__", + [](qc::CompoundOperation* self, + const std::vector& ops) { + std::vector> uniqueOps; + uniqueOps.reserve(ops.size()); + for (const auto& op : ops) { + assert(op != nullptr && "ops must not contain nullptr"); + uniqueOps.emplace_back(op->clone()); + } + new (self) qc::CompoundOperation(std::move(uniqueOps)); + }, + "ops"_a) + + .def("__len__", &qc::CompoundOperation::size, + "The number of operations in the compound operation.") + .def( "__getitem__", - [&wrap](const qc::CompoundOperation& op, DiffType i) { + [wrap](const qc::CompoundOperation& op, DiffType i) { i = wrap(i, op.size()); return op.at(static_cast(i)).get(); }, - py::return_value_policy::reference_internal, "index"_a) + nb::rv_policy::reference_internal, "index"_a, + R"pb(Get the operation at the given index. + +Note: + This gives direct access to the operations in the compound operation + +Args: + index: The index of the operation to get. + +Returns: + The operation at the given index.)pb") + .def( "__getitem__", - [](const qc::CompoundOperation& op, const py::slice& slice) { - std::size_t start{}; - std::size_t stop{}; - std::size_t step{}; - std::size_t sliceLength{}; - if (!slice.compute(op.size(), &start, &stop, &step, &sliceLength)) { - throw py::error_already_set(); - } + [](const qc::CompoundOperation& op, const nb::slice& slice) { + auto [start, stop, step, sliceLength] = slice.compute(op.size()); auto ops = std::vector(); ops.reserve(sliceLength); - for (std::size_t i = start; i < stop; i += step) { - ops.emplace_back(op.at(i).get()); + for (std::size_t i = 0; i < sliceLength; ++i) { + auto idx = static_cast(start) + + (static_cast(i) * step); + ops.emplace_back(op.at(static_cast(idx)).get()); } return ops; }, - py::return_value_policy::reference_internal, "index"_a) + nb::rv_policy::reference_internal, "index"_a, + R"pb(Get the operations in the given slice. + +Note: + This gives direct access to the operations in the compound operation. + +Args: + index: The slice of the operations to get. + +Returns: + The operations in the given slice.)pb") + .def( "__setitem__", - [&wrap](qc::CompoundOperation& compOp, DiffType i, - const qc::Operation& op) { + [wrap](qc::CompoundOperation& compOp, DiffType i, + const qc::Operation& op) { i = wrap(i, compOp.size()); compOp[static_cast(i)] = op.clone(); }, - "index"_a, "value"_a) + "index"_a, "value"_a, R"pb(Set the operation at the given index. + +Args: + index: The index of the operation to set. + value: The operation to set at the given index.)pb") + .def( "__setitem__", - [](qc::CompoundOperation& compOp, const py::slice& slice, + [](qc::CompoundOperation& compOp, const nb::slice& slice, const std::vector& ops) { - std::size_t start{}; - std::size_t stop{}; - std::size_t step{}; - std::size_t sliceLength{}; - if (!slice.compute(compOp.size(), &start, &stop, &step, - &sliceLength)) { - throw py::error_already_set(); - } + auto [start, stop, step, sliceLength] = + slice.compute(compOp.size()); if (sliceLength != ops.size()) { throw std::runtime_error( "Length of slice and number of operations do not match."); } for (std::size_t i = 0; i < sliceLength; ++i) { - compOp[start] = ops[i]->clone(); + assert(ops[i] != nullptr && "ops must not contain nullptr"); + compOp[static_cast(start)] = ops[i]->clone(); start += step; } }, - "index"_a, "value"_a) + nb::sig("def __setitem__(self, index: slice, value: " + "collections.abc.Iterable[mqt.core.ir.operations.Operation]) " + "-> None"), + R"pb(Set the operations in the given slice. + +Args: + index: The slice of operations to set. + value: The operations to set in the given slice.)pb") + .def( "__delitem__", - [&wrap](qc::CompoundOperation& compOp, DiffType i) { - i = wrap(i, compOp.size()); - compOp.erase(compOp.begin() + i); + [wrap](qc::CompoundOperation& op, DiffType i) { + i = wrap(i, op.size()); + op.erase(op.begin() + i); }, - "index"_a) + "index"_a, R"pb(Delete the operation at the given index. + +Args: + index: The index of the operation to delete.)pb") + .def( "__delitem__", - [](qc::CompoundOperation& compOp, const py::slice& slice) { - std::size_t start{}; - std::size_t stop{}; - std::size_t step{}; - std::size_t sliceLength{}; - if (!slice.compute(compOp.size(), &start, &stop, &step, - &sliceLength)) { - throw py::error_already_set(); + [](qc::CompoundOperation& op, const nb::slice& slice) { + auto [start, stop, step, sliceLength] = slice.compute(op.size()); + // Delete in reverse order to not invalidate indices + std::vector indices; + indices.reserve(sliceLength); + for (std::size_t i = 0; i < sliceLength; ++i) { + indices.emplace_back(static_cast(start) + + (static_cast(i) * step)); } - // delete in reverse order to not invalidate indices - for (std::size_t i = sliceLength; i > 0; --i) { - compOp.erase(compOp.begin() + - static_cast(start + ((i - 1) * step))); + std::ranges::sort(indices, std::greater<>()); + for (const auto idx : indices) { + op.erase(op.begin() + idx); } }, - "index"_a) + "index"_a, R"pb(Delete the operations in the given slice. + +Args: + index: The slice of operations to delete.)pb") + .def( "append", [](qc::CompoundOperation& compOp, const qc::Operation& op) { compOp.emplace_back(op.clone()); }, - "value"_a) + "value"_a, "Append an operation to the compound operation.") + .def( "insert", [](qc::CompoundOperation& compOp, const std::size_t idx, @@ -153,9 +205,18 @@ void registerCompoundOperation(const py::module& m) { compOp.insert(compOp.begin() + static_cast(idx), op.clone()); }, - "index"_a, "value"_a) - .def("empty", &qc::CompoundOperation::empty) - .def("clear", &qc::CompoundOperation::clear) + "index"_a, "value"_a, R"pb(Insert an operation at the given index. + +Args: + index: The index to insert the operation at. + value: The operation to insert.)pb") + + .def("empty", &qc::CompoundOperation::empty, + "Check if the compound operation is empty.") + + .def("clear", &qc::CompoundOperation::clear, + "Clear all operations in the compound operation.") + .def("__repr__", [](const qc::CompoundOperation& op) { std::stringstream ss; ss << "CompoundOperation([..." << op.size() << " ops...])"; diff --git a/bindings/ir/operations/register_control.cpp b/bindings/ir/operations/register_control.cpp index e8796729cc..36ce5196e3 100644 --- a/bindings/ir/operations/register_control.cpp +++ b/bindings/ir/operations/register_control.cpp @@ -11,43 +11,48 @@ #include "ir/Definitions.hpp" #include "ir/operations/Control.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -#include -#include -// clang-format on +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerControl(const py::module& m) { +void registerControl(const nb::module_& m) { + auto control = nb::class_( + m, "Control", + R"pb(A control is a pair of a qubit and a type. The type can be either positive or negative. - auto control = py::class_(m, "Control"); +Args: + qubit: The qubit that is the control. + type_: The type of the control.)pb"); - py::native_enum(control, "Type", "enum.Enum", - "Enumeration of control types.") + nb::enum_(control, "Type", "Enumeration of control types.") .value("Pos", qc::Control::Type::Pos) - .value("Neg", qc::Control::Type::Neg) - .finalize(); + .value("Neg", qc::Control::Type::Neg); + + control.def(nb::init(), "qubit"_a, + "type_"_a.sig("...") = qc::Control::Type::Pos); + + control.def_ro("qubit", &qc::Control::qubit, + "The qubit that is the control."); + + control.def_ro("type_", &qc::Control::type, "The type of the control."); - control.def(py::init(), "qubit"_a, - "type_"_a = qc::Control::Type::Pos); - control.def_readwrite("type_", &qc::Control::type); - control.def_readwrite("qubit", &qc::Control::qubit); control.def("__str__", [](const qc::Control& c) { return c.toString(); }); control.def("__repr__", [](const qc::Control& c) { return c.toString(); }); - control.def(py::self == py::self); // NOLINT(misc-redundant-expression) - control.def(py::self != py::self); // NOLINT(misc-redundant-expression) - control.def(hash(py::self)); - py::implicitly_convertible(); + + control.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + control.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); + control.def(nb::hash(nb::self)); + + nb::implicitly_convertible(); } } // namespace mqt diff --git a/bindings/ir/operations/register_if_else_operation.cpp b/bindings/ir/operations/register_if_else_operation.cpp index 947a087e02..928af900aa 100644 --- a/bindings/ir/operations/register_if_else_operation.cpp +++ b/bindings/ir/operations/register_if_else_operation.cpp @@ -13,87 +13,112 @@ #include "ir/operations/IfElseOperation.hpp" #include "ir/operations/Operation.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -// clang-format on - #include #include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerIfElseOperation(const py::module& m) { - py::native_enum( - m, "ComparisonKind", "enum.Enum", +void registerIfElseOperation(const nb::module_& m) { + nb::enum_( + m, "ComparisonKind", "Enumeration of comparison types for classic-controlled operations.") - .value("eq", qc::ComparisonKind::Eq) - .value("neq", qc::ComparisonKind::Neq) - .value("lt", qc::ComparisonKind::Lt) - .value("leq", qc::ComparisonKind::Leq) - .value("gt", qc::ComparisonKind::Gt) - .value("geq", qc::ComparisonKind::Geq) - .export_values() - .finalize(); - - auto ifElse = - py::class_(m, "IfElseOperation"); - - ifElse.def(py::init([](qc::Operation* thenOp, qc::Operation* elseOp, - qc::ClassicalRegister& controlReg, - const std::uint64_t expectedVal, - const qc::ComparisonKind kind) { - std::unique_ptr thenPtr = - thenOp ? thenOp->clone() : nullptr; - std::unique_ptr elsePtr = - elseOp ? elseOp->clone() : nullptr; - return std::make_unique( - std::move(thenPtr), std::move(elsePtr), controlReg, - expectedVal, kind); - }), - "then_operation"_a, "else_operation"_a, "control_register"_a, - "expected_value"_a = 1U, - "comparison_kind"_a = qc::ComparisonKind::Eq); - ifElse.def(py::init([](qc::Operation* thenOp, qc::Operation* elseOp, - qc::Bit controlBit, std::uint64_t expectedVal, - qc::ComparisonKind kind) { - std::unique_ptr thenPtr = - thenOp ? thenOp->clone() : nullptr; - std::unique_ptr elsePtr = - elseOp ? elseOp->clone() : nullptr; - return std::make_unique( - std::move(thenPtr), std::move(elsePtr), controlBit, - expectedVal, kind); - }), - "then_operation"_a, "else_operation"_a, "control_bit"_a, - "expected_value"_a = 1U, - "comparison_kind"_a = qc::ComparisonKind::Eq); - ifElse.def_property_readonly("then_operation", - &qc::IfElseOperation::getThenOp, - py::return_value_policy::reference_internal); - ifElse.def_property_readonly("else_operation", - &qc::IfElseOperation::getElseOp, - py::return_value_policy::reference_internal); - ifElse.def_property_readonly("control_register", - &qc::IfElseOperation::getControlRegister); - ifElse.def_property_readonly("control_bit", - &qc::IfElseOperation::getControlBit); - ifElse.def_property_readonly("expected_value_register", - &qc::IfElseOperation::getExpectedValueRegister); - ifElse.def_property_readonly("expected_value_bit", - &qc::IfElseOperation::getExpectedValueBit); - ifElse.def_property_readonly("comparison_kind", - &qc::IfElseOperation::getComparisonKind); + .value("eq", qc::ComparisonKind::Eq, "Equality comparison.") + .value("neq", qc::ComparisonKind::Neq, "Inequality comparison.") + .value("lt", qc::ComparisonKind::Lt, "Less-than comparison.") + .value("leq", qc::ComparisonKind::Leq, "Less-than-or-equal comparison.") + .value("gt", qc::ComparisonKind::Gt, "Greater-than comparison.") + .value("geq", qc::ComparisonKind::Geq, + "Greater-than-or-equal comparison."); + + auto ifElse = nb::class_( + m, "IfElseOperation", R"pb(If-else quantum operation. + +This class is used to represent an if-else operation. +The then operation is executed if the value of the classical register matches the expected value. +Otherwise, the else operation is executed. + +Args: + then_operation: The operation that is executed if the condition is met. + else_operation: The operation that is executed if the condition is not met. + control_register: The classical register that controls the operation. + expected_value: The expected value of the classical register. + comparison_kind: The kind of comparison (default is equality).)pb"); + + ifElse.def( + "__init__", + [](qc::IfElseOperation* self, qc::Operation* thenOp, + qc::Operation* elseOp, qc::ClassicalRegister& controlReg, + const std::uint64_t expectedVal, const qc::ComparisonKind kind) { + std::unique_ptr thenPtr = + thenOp ? thenOp->clone() : nullptr; + std::unique_ptr elsePtr = + elseOp ? elseOp->clone() : nullptr; + new (self) qc::IfElseOperation(std::move(thenPtr), std::move(elsePtr), + controlReg, expectedVal, kind); + }, + "then_operation"_a, nb::arg("else_operation").none(true), + "control_register"_a, "expected_value"_a = 1U, + "comparison_kind"_a = qc::ComparisonKind::Eq); + ifElse.def( + "__init__", + [](qc::IfElseOperation* self, qc::Operation* thenOp, + qc::Operation* elseOp, qc::Bit controlBit, bool expectedVal, + qc::ComparisonKind kind) { + std::unique_ptr thenPtr = + thenOp ? thenOp->clone() : nullptr; + std::unique_ptr elsePtr = + elseOp ? elseOp->clone() : nullptr; + new (self) qc::IfElseOperation(std::move(thenPtr), std::move(elsePtr), + controlBit, expectedVal, kind); + }, + "then_operation"_a, nb::arg("else_operation").none(true), "control_bit"_a, + "expected_value"_a = true, "comparison_kind"_a = qc::ComparisonKind::Eq); + + ifElse.def_prop_ro("then_operation", &qc::IfElseOperation::getThenOp, + nb::rv_policy::reference_internal, + "The operation that is executed if the condition is met."); + + ifElse.def_prop_ro( + "else_operation", &qc::IfElseOperation::getElseOp, + nb::rv_policy::reference_internal, + nb::sig("def else_operation(self) -> " + "mqt.core.ir.operations.Operation | None"), + "The operation that is executed if the condition is not met."); + + ifElse.def_prop_ro("control_register", + &qc::IfElseOperation::getControlRegister, + "The classical register that controls the operation."); + + ifElse.def_prop_ro("control_bit", &qc::IfElseOperation::getControlBit, + "The classical bit that controls the operation."); + + ifElse.def_prop_ro("expected_value_register", + &qc::IfElseOperation::getExpectedValueRegister, + R"pb(The expected value of the classical register. + +The then-operation is executed if the value of the classical register matches the expected value based on the kind of comparison. +The expected value is an integer that is interpreted as a binary number, where the least significant bit is at the start index of the classical register.)pb"); + + ifElse.def_prop_ro("expected_value_bit", + &qc::IfElseOperation::getExpectedValueBit, + R"pb(The expected value of the classical bit. + +The then-operation is executed if the value of the classical bit matches the expected value based on the kind of comparison.)pb"); + + ifElse.def_prop_ro("comparison_kind", &qc::IfElseOperation::getComparisonKind, + R"pb(The kind of comparison. + +The then-operation is executed if the value of the control matches the expected value based on the kind of comparison.)pb"); + ifElse.def("__repr__", [](const qc::IfElseOperation& op) { std::stringstream ss; ss << "IfElseOperation(<...then-op...>, <...else-op...>, "; diff --git a/bindings/ir/operations/register_non_unitary_operation.cpp b/bindings/ir/operations/register_non_unitary_operation.cpp index e14e23db25..cf096d0d14 100644 --- a/bindings/ir/operations/register_non_unitary_operation.cpp +++ b/bindings/ir/operations/register_non_unitary_operation.cpp @@ -13,33 +13,41 @@ #include "ir/operations/OpType.hpp" #include "ir/operations/Operation.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -// clang-format on - +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerNonUnitaryOperation(const py::module& m) { - py::class_(m, "NonUnitaryOperation") - .def(py::init, std::vector>(), +void registerNonUnitaryOperation(const nb::module_& m) { + nb::class_(m, "NonUnitaryOperation", + R"pb(Non-unitary operation. + +This class is used to represent all non-unitary operations, i.e., operations that are not reversible. +This includes measurements and resets. + +Args: + targets: The target qubit(s) of the operation. + classics: The classical bit(s) that are associated with the operation (only relevant for measurements). + op_type: The type of the operation.)pb") + + .def(nb::init, std::vector>(), "targets"_a, "classics"_a) - .def(py::init(), "target"_a, "classic"_a) - .def(py::init, qc::OpType>(), "targets"_a, + .def(nb::init(), "target"_a, "classic"_a) + .def(nb::init, qc::OpType>(), "targets"_a, "op_type"_a = qc::OpType::Reset) - .def_property_readonly( - "classics", py::overload_cast<>(&qc::NonUnitaryOperation::getClassics, - py::const_)) + + .def_prop_ro("classics", + nb::overload_cast<>(&qc::NonUnitaryOperation::getClassics, + nb::const_), + "The classical bits that are associated with the operation.") + .def("__repr__", [](const qc::NonUnitaryOperation& op) { std::stringstream ss; ss << "NonUnitaryOperation("; diff --git a/bindings/ir/operations/register_operation.cpp b/bindings/ir/operations/register_operation.cpp index e316eeb6ef..466de6134a 100644 --- a/bindings/ir/operations/register_operation.cpp +++ b/bindings/ir/operations/register_operation.cpp @@ -11,65 +11,156 @@ #include "ir/operations/Control.hpp" #include "ir/operations/Operation.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -// clang-format on - +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerOperation(const py::module& m) { - py::class_(m, "Operation") - .def_property_readonly("name", &qc::Operation::getName) - .def_property("type_", &qc::Operation::getType, &qc::Operation::setGate) - .def_property( +void registerOperation(const nb::module_& m) { + nb::class_(m, "Operation") + .def_prop_ro("name", &qc::Operation::getName, + "The name of the operation.") + + .def_prop_rw("type_", &qc::Operation::getType, &qc::Operation::setGate, + "The type of the operation.") + + .def_prop_rw( "targets", [](const qc::Operation& op) { return op.getTargets(); }, - &qc::Operation::setTargets) - .def_property_readonly("num_targets", &qc::Operation::getNtargets) - .def_property( + &qc::Operation::setTargets, R"pb(The targets of the operation. + +Note: + The notion of a target might not make sense for all types of operations.)pb") + + .def_prop_ro("num_targets", &qc::Operation::getNtargets, + "The number of targets of the operation.") + + .def_prop_rw( "controls", [](const qc::Operation& op) { return op.getControls(); }, - &qc::Operation::setControls) - .def_property_readonly("num_controls", &qc::Operation::getNcontrols) - .def("add_control", &qc::Operation::addControl, "control"_a) - .def("add_controls", &qc::Operation::addControls, "controls"_a) - .def("clear_controls", &qc::Operation::clearControls) + &qc::Operation::setControls, R"pb(The controls of the operation. + +Note: + The notion of a control might not make sense for all types of operations.)pb") + + .def_prop_ro("num_controls", &qc::Operation::getNcontrols, + "The number of controls of the operation.") + + .def("add_control", &qc::Operation::addControl, "control"_a, + R"pb(Add a control to the operation. + +Args: + control: The control to add.)pb") + + .def("add_controls", &qc::Operation::addControls, "controls"_a, + R"pb(Add multiple controls to the operation. + +Args: + controls: The controls to add.)pb") + + .def("clear_controls", &qc::Operation::clearControls, + "Clear all controls of the operation.") + .def( "remove_control", [](qc::Operation& op, const qc::Control& c) { op.removeControl(c); }, - "control"_a) - .def("remove_controls", &qc::Operation::removeControls, "controls"_a) - .def("get_used_qubits", &qc::Operation::getUsedQubits) - .def("acts_on", &qc::Operation::actsOn, "qubit"_a) - .def_property( + "control"_a, R"pb(Remove a control from the operation. + +Args: + control: The control to remove.)pb") + + .def("remove_controls", &qc::Operation::removeControls, "controls"_a, + R"pb(Remove multiple controls from the operation. + +Args: + controls: The controls to remove.)pb") + + .def("get_used_qubits", &qc::Operation::getUsedQubits, + R"pb(Get the qubits that are used by the operation. + +Returns: + The set of qubits that are used by the operation.)pb") + + .def("acts_on", &qc::Operation::actsOn, "qubit"_a, + R"pb(Check if the operation acts on a specific qubit. + +Args: + qubit: The qubit to check. + +Returns: + True if the operation acts on the qubit, False otherwise.)pb") + + .def_prop_rw( "parameter", [](const qc::Operation& op) { return op.getParameter(); }, - &qc::Operation::setParameter) - .def("is_unitary", &qc::Operation::isUnitary) - .def("is_standard_operation", &qc::Operation::isStandardOperation) - .def("is_compound_operation", &qc::Operation::isCompoundOperation) - .def("is_non_unitary_operation", &qc::Operation::isNonUnitaryOperation) - .def("is_if_else_operation", &qc::Operation::isIfElseOperation) - .def("is_symbolic_operation", &qc::Operation::isSymbolicOperation) - .def("is_controlled", &qc::Operation::isControlled) - .def("get_inverted", &qc::Operation::getInverted) - .def("invert", &qc::Operation::invert) - .def("__eq__", [](const qc::Operation& op, - const qc::Operation& other) { return op == other; }) - .def("__ne__", [](const qc::Operation& op, - const qc::Operation& other) { return op != other; }) - .def("__hash__", - [](const qc::Operation& op) { - return std::hash{}(op); - }) + &qc::Operation::setParameter, R"pb(The parameters of the operation. + +Note: + The notion of a parameter might not make sense for all types of operations.)pb") + + .def("is_unitary", &qc::Operation::isUnitary, + R"pb(Check if the operation is unitary. + +Returns: + True if the operation is unitary, False otherwise.)pb") + + .def("is_standard_operation", &qc::Operation::isStandardOperation, + R"pb(Check if the operation is a :class:`StandardOperation`. + +Returns: + True if the operation is a :class:`StandardOperation`, False otherwise.)pb") + + .def("is_compound_operation", &qc::Operation::isCompoundOperation, + R"pb(Check if the operation is a :class:`CompoundOperation`. + +Returns: + True if the operation is a :class:`CompoundOperation`, False otherwise.)pb") + + .def("is_non_unitary_operation", &qc::Operation::isNonUnitaryOperation, + R"pb(Check if the operation is a :class:`NonUnitaryOperation`. + +Returns: + True if the operation is a :class:`NonUnitaryOperation`, False otherwise.)pb") + + .def("is_if_else_operation", &qc::Operation::isIfElseOperation, + R"pb(Check if the operation is a :class:`IfElseOperation`. + +Returns: + True if the operation is a :class:`IfElseOperation`, False otherwise.)pb") + + .def("is_symbolic_operation", &qc::Operation::isSymbolicOperation, + R"pb(Check if the operation is a :class:`SymbolicOperation`. + +Returns: + True if the operation is a :class:`SymbolicOperation`, False otherwise.)pb") + + .def("is_controlled", &qc::Operation::isControlled, + R"pb(Check if the operation is controlled. + +Returns: + True if the operation is controlled, False otherwise.)pb") + + .def("get_inverted", &qc::Operation::getInverted, + R"pb(Get the inverse of the operation. + +Returns: + The inverse of the operation.)pb") + + .def("invert", &qc::Operation::invert, "Invert the operation (in-place).") + + .def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")) + .def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")) + .def(nb::hash(nb::self)) + .def("__repr__", [](const qc::Operation& op) { std::ostringstream oss; oss << "Operation(type=" << op.getType() << ", ...)"; diff --git a/bindings/ir/operations/register_optype.cpp b/bindings/ir/operations/register_optype.cpp index 3cc10f3e48..dad27e434d 100644 --- a/bindings/ir/operations/register_optype.cpp +++ b/bindings/ir/operations/register_optype.cpp @@ -10,65 +10,223 @@ #include "ir/operations/OpType.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -// clang-format on +#include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerOptype(const py::module& m) { - py::native_enum(m, "OpType", "enum.Enum", - "Enumeration of operation types.") - .value("none", qc::OpType::None) - .value("gphase", qc::OpType::GPhase) - .value("i", qc::OpType::I) - .value("h", qc::OpType::H) - .value("x", qc::OpType::X) - .value("y", qc::OpType::Y) - .value("z", qc::OpType::Z) - .value("s", qc::OpType::S) - .value("sdg", qc::OpType::Sdg) - .value("t", qc::OpType::T) - .value("tdg", qc::OpType::Tdg) - .value("v", qc::OpType::V) - .value("vdg", qc::OpType::Vdg) - .value("u", qc::OpType::U) - .value("u2", qc::OpType::U2) - .value("p", qc::OpType::P) - .value("sx", qc::OpType::SX) - .value("sxdg", qc::OpType::SXdg) - .value("rx", qc::OpType::RX) - .value("ry", qc::OpType::RY) - .value("rz", qc::OpType::RZ) - .value("r", qc::OpType::R) - .value("swap", qc::OpType::SWAP) - .value("iswap", qc::OpType::iSWAP) - .value("iswapdg", qc::OpType::iSWAPdg) - .value("peres", qc::OpType::Peres) - .value("peresdg", qc::OpType::Peresdg) - .value("dcx", qc::OpType::DCX) - .value("ecr", qc::OpType::ECR) - .value("rxx", qc::OpType::RXX) - .value("ryy", qc::OpType::RYY) - .value("rzz", qc::OpType::RZZ) - .value("rzx", qc::OpType::RZX) - .value("xx_minus_yy", qc::OpType::XXminusYY) - .value("xx_plus_yy", qc::OpType::XXplusYY) - .value("compound", qc::OpType::Compound) - .value("measure", qc::OpType::Measure) - .value("reset", qc::OpType::Reset) - .value("barrier", qc::OpType::Barrier) - .value("if_else", qc::OpType::IfElse) - .export_values() - .finalize(); +void registerOptype(const nb::module_& m) { + nb::enum_(m, "OpType", "Enumeration of operation types.") + + .value("none", qc::OpType::None, R"pb(A placeholder operation. + +It is used to represent an operation that is not yet defined.)pb") + + .value("gphase", qc::OpType::GPhase, R"pb(A global phase operation. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.gphase`)pb") + + .value("i", qc::OpType::I, R"pb(An identity operation. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.i`)pb") + + .value("h", qc::OpType::H, R"pb(A Hadamard gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.h`)pb") + + .value("x", qc::OpType::X, R"pb(An X gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.x`)pb") + + .value("y", qc::OpType::Y, R"pb(A Y gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.y`)pb") + + .value("z", qc::OpType::Z, R"pb(A Z gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.z`)pb") + + .value("s", qc::OpType::S, R"pb(An S gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.s`)pb") + + .value("sdg", qc::OpType::Sdg, R"pb(An :math:`S^\dagger` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.sdg`)pb") + + .value("t", qc::OpType::T, R"pb(A T gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.t`)pb") + + .value("tdg", qc::OpType::Tdg, R"pb(A :math:`T^\dagger` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.tdg`)pb") + + .value("v", qc::OpType::V, R"pb(A V gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.v`)pb") + + .value("vdg", qc::OpType::Vdg, R"pb(A :math:`V^\dagger` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.vdg`)pb") + + .value("u", qc::OpType::U, R"pb(A U gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.u`)pb") + + .value("u2", qc::OpType::U2, R"pb(A U2 gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.u2`)pb") + + .value("p", qc::OpType::P, R"pb(A phase gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.p`)pb") + + .value("sx", qc::OpType::SX, R"pb(A :math:`\sqrt{X}` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.sx`)pb") + + .value("sxdg", qc::OpType::SXdg, R"pb(A :math:`\sqrt{X}^\dagger` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.sxdg`)pb") + + .value("rx", qc::OpType::RX, R"pb(A :math:`R_x` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.rx`)pb") + + .value("ry", qc::OpType::RY, R"pb(A :math:`R_y` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.ry`)pb") + + .value("rz", qc::OpType::RZ, R"pb(A :math:`R_z` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.rz`)pb") + + .value("r", qc::OpType::R, R"pb(An :math:`R` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.r`)pb") + + .value("swap", qc::OpType::SWAP, R"pb(A SWAP gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.swap`)pb") + + .value("iswap", qc::OpType::iSWAP, R"pb(A iSWAP gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.iswap`)pb") + + .value("iswapdg", qc::OpType::iSWAPdg, + R"pb(A :math:`i\text{SWAP}^\dagger` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.iswapdg`)pb") + + .value("peres", qc::OpType::Peres, R"pb(A Peres gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.peres`)pb") + + .value("peresdg", qc::OpType::Peresdg, + R"pb(A :math:`\text{Peres}^\dagger` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.peresdg`)pb") + + .value("dcx", qc::OpType::DCX, R"pb(A DCX gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.dcx`)pb") + + .value("ecr", qc::OpType::ECR, R"pb(An ECR gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.ecr`)pb") + + .value("rxx", qc::OpType::RXX, R"pb(A :math:`R_{xx}` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.rxx`)pb") + + .value("ryy", qc::OpType::RYY, R"pb(A :math:`R_{yy}` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.ryy`)pb") + + .value("rzz", qc::OpType::RZZ, R"pb(A :math:`R_{zz}` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.rzz`)pb") + + .value("rzx", qc::OpType::RZX, R"pb(A :math:`R_{zx}` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.rzx`)pb") + + .value("xx_minus_yy", qc::OpType::XXminusYY, + R"pb(A :math:`R_{XX - YY}` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.xx_minus_yy`)pb") + + .value("xx_plus_yy", qc::OpType::XXplusYY, + R"pb(A :math:`R_{XX + YY}` gate. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.xx_plus_yy`)pb") + + .value("compound", qc::OpType::Compound, R"pb(A compound operation. + +It is used to group multiple operations into a single operation. + +See also :class:`.CompoundOperation`)pb") + + .value("measure", qc::OpType::Measure, R"pb(A measurement operation. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.measure`)pb") + + .value("reset", qc::OpType::Reset, R"pb(A reset operation. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.reset`)pb") + + .value("barrier", qc::OpType::Barrier, R"pb(A barrier operation. + +It is used to separate operations in the circuit. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.barrier`)pb") + + .value("if_else", qc::OpType::IfElse, R"pb(An if-else operation. + +It is used to control the execution of an operation based on the value of a classical register. + +See Also: + :meth:`mqt.core.ir.QuantumComputation.if_else`)pb"); } } // namespace mqt diff --git a/bindings/ir/operations/register_standard_operation.cpp b/bindings/ir/operations/register_standard_operation.cpp index 461105798b..9a5ec678f4 100644 --- a/bindings/ir/operations/register_standard_operation.cpp +++ b/bindings/ir/operations/register_standard_operation.cpp @@ -14,50 +14,59 @@ #include "ir/operations/Operation.hpp" #include "ir/operations/StandardOperation.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -// clang-format on - +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerStandardOperation(const py::module& m) { - py::class_(m, "StandardOperation") - .def(py::init<>()) - .def(py::init>(), "target"_a, - "op_type"_a, "params"_a = std::vector{}) - .def(py::init>(), - "targets"_a, "op_type"_a, "params"_a = std::vector{}) - .def(py::init( + m, "StandardOperation", R"pb(Standard quantum operation. + +This class is used to represent all standard quantum operations, i.e., operations that are unitary. +This includes all possible quantum gates. +Such Operations are defined by their :class:`OpType`, the qubits (controls and targets) they act on, and their parameters. + +Args: + control: The control qubit(s) of the operation (if any). + target: The target qubit(s) of the operation. + op_type: The type of the operation. + params: The parameters of the operation (if any).)pb") + + .def(nb::init<>()) + .def(nb::init>(), "target"_a, + "op_type"_a, "params"_a.sig("...") = std::vector{}) + .def(nb::init>(), + "targets"_a, "op_type"_a, + "params"_a.sig("...") = std::vector{}) + .def(nb::init&>(), "control"_a, "target"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init&>(), "control"_a, "targets"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init&>(), "controls"_a, "target"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init>(), "controls"_a, "targets"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init>(), "controls"_a, "target0"_a, "target1"_a, "op_type"_a, - "params"_a = std::vector{}) + "params"_a.sig("...") = std::vector{}) .def("__repr__", [](const qc::StandardOperation& op) { std::stringstream ss; ss << "StandardOperation("; diff --git a/bindings/ir/operations/register_symbolic_operation.cpp b/bindings/ir/operations/register_symbolic_operation.cpp index 997ab6193d..5da9e1516a 100644 --- a/bindings/ir/operations/register_symbolic_operation.cpp +++ b/bindings/ir/operations/register_symbolic_operation.cpp @@ -15,62 +15,95 @@ #include "ir/operations/StandardOperation.hpp" #include "ir/operations/SymbolicOperation.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -// clang-format on - +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerSymbolicOperation(const py::module& m) { - py::class_( +void registerSymbolicOperation(const nb::module_& m) { + nb::class_( m, "SymbolicOperation", - "Class representing a symbolic operation." - "This encompasses all symbolic versions of `StandardOperation` that " - "involve (float) angle parameters.") - .def(py::init<>(), "Create an empty symbolic operation.") - .def(py::init()) + .def(nb::init&>(), "target"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init&>(), "targets"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init&>(), "control"_a, "target"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init&>(), "control"_a, "targets"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init&>(), "controls"_a, "target"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init&>(), "controls"_a, "targets"_a, "op_type"_a, - "params"_a = std::vector{}) - .def(py::init{}) + .def(nb::init&>(), "controls"_a, "target0"_a, "target1"_a, "op_type"_a, - "params"_a = std::vector{}) - .def("get_parameter", &qc::SymbolicOperation::getParameter) - .def("get_parameters", &qc::SymbolicOperation::getParameters) + "params"_a.sig("...") = std::vector{}) + .def("get_parameter", &qc::SymbolicOperation::getParameter, "index"_a, + R"pb(Get the parameter at the given index. + +Args: + index: The index of the parameter to get. + +Returns: + The parameter at the given index.)pb") + + .def("get_parameters", &qc::SymbolicOperation::getParameters, + R"pb(Get all parameters of the operation. + +Returns: + The parameters of the operation.)pb") + .def("get_instantiated_operation", - &qc::SymbolicOperation::getInstantiatedOperation, "assignment"_a) - .def("instantiate", &qc::SymbolicOperation::instantiate, "assignment"_a); + &qc::SymbolicOperation::getInstantiatedOperation, "assignment"_a, + R"pb(Get the instantiated operation. + +Args: + assignment: The assignment of the symbolic parameters. + +Returns: + The instantiated operation.)pb") + + .def("instantiate", &qc::SymbolicOperation::instantiate, "assignment"_a, + R"pb(Instantiate the operation (in-place). + +Args: + assignment: The assignment of the symbolic parameters.)pb"); } } // namespace mqt diff --git a/bindings/ir/register_ir.cpp b/bindings/ir/register_ir.cpp index 00cd53c340..084006b3b9 100644 --- a/bindings/ir/register_ir.cpp +++ b/bindings/ir/register_ir.cpp @@ -8,33 +8,29 @@ * Licensed under the MIT License */ -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) -// clang-format on +#include namespace mqt { -namespace py = pybind11; -using namespace py::literals; +namespace nb = nanobind; // forward declarations -void registerRegisters(py::module& m); -void registerPermutation(py::module& m); -void registerOperations(py::module& m); -void registerSymbolic(py::module& m); -void registerQuantumComputation(py::module& m); +void registerRegisters(const nb::module_& m); +void registerPermutation(const nb::module_& m); +void registerOperations(const nb::module_& m); +void registerSymbolic(const nb::module_& m); +void registerQuantumComputation(const nb::module_& m); -PYBIND11_MODULE(MQT_CORE_MODULE_NAME, m, py::mod_gil_not_used()) { +NB_MODULE(MQT_CORE_MODULE_NAME, m) { registerPermutation(m); - py::module registers = m.def_submodule("registers"); - registerRegisters(registers); - py::module symbolic = m.def_submodule("symbolic"); + const nb::module_ symbolic = m.def_submodule("symbolic"); registerSymbolic(symbolic); - py::module operations = m.def_submodule("operations"); + const nb::module_ registers = m.def_submodule("registers"); + registerRegisters(registers); + + const nb::module_ operations = m.def_submodule("operations"); registerOperations(operations); registerQuantumComputation(m); diff --git a/bindings/ir/register_operations.cpp b/bindings/ir/register_operations.cpp index d8250ed915..48c41fbd0e 100644 --- a/bindings/ir/register_operations.cpp +++ b/bindings/ir/register_operations.cpp @@ -8,29 +8,24 @@ * Licensed under the MIT License */ -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) -// clang-format on +#include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; // forward declarations -void registerOptype(const py::module& m); -void registerControl(const py::module& m); -void registerOperation(const py::module& m); -void registerStandardOperation(const py::module& m); -void registerCompoundOperation(const py::module& m); -void registerNonUnitaryOperation(const py::module& m); -void registerSymbolicOperation(const py::module& m); -void registerIfElseOperation(const py::module& m); +void registerOptype(const nb::module_& m); +void registerControl(const nb::module_& m); +void registerOperation(const nb::module_& m); +void registerStandardOperation(const nb::module_& m); +void registerCompoundOperation(const nb::module_& m); +void registerNonUnitaryOperation(const nb::module_& m); +void registerSymbolicOperation(const nb::module_& m); +void registerIfElseOperation(const nb::module_& m); // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerOperations(py::module& m) { +void registerOperations(const nb::module_& m) { registerOptype(m); registerControl(m); registerOperation(m); diff --git a/bindings/ir/register_permutation.cpp b/bindings/ir/register_permutation.cpp index 237112bc71..a7d6760b69 100644 --- a/bindings/ir/register_permutation.cpp +++ b/bindings/ir/register_permutation.cpp @@ -12,66 +12,177 @@ #include "ir/Permutation.hpp" #include "ir/operations/Control.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -#include -#include -// clang-format on - +#include #include +#include +#include +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include +#include +#include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; + +namespace { + +qc::Qubit nbIntToQubit(const nb::int_& value) { + const auto valueInt = static_cast(value); + if (valueInt < 0) { + throw nb::value_error("Qubit index cannot be negative"); + } + const auto valueUint = static_cast(valueInt); + if (valueUint > std::numeric_limits::max()) { + throw nb::value_error("Qubit index exceeds maximum value"); + } + return static_cast(valueUint); +} + +} // namespace // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerPermutation(py::module& m) { - py::class_(m, "Permutation") - .def(py::init<>()) - .def(py::init([](const py::dict& p) { - qc::Permutation perm; - for (const auto& [key, value] : p) { - perm[key.cast()] = value.cast(); - } - return perm; - }), - "perm"_a, "Create a permutation from a dictionary.") +void registerPermutation(const nb::module_& m) { + nb::class_( + m, "Permutation", + nb::sig("class Permutation(collections.abc.MutableMapping[int, int])"), + R"pb(A class to represent a permutation of the qubits in a quantum circuit. + +Args: + permutation: The permutation to initialize the object with.)pb") + + .def(nb::init<>()) + + .def( + "__init__", + [](qc::Permutation* self, + const nb::typed& p) { + qc::Permutation perm; + for (const auto& [key, value] : p) { + const auto keyQubit = nbIntToQubit(static_cast(key)); + const auto valueQubit = + nbIntToQubit(static_cast(value)); + perm[keyQubit] = valueQubit; + } + new (self) qc::Permutation(std::move(perm)); + }, + "permutation"_a, "Create a permutation from a dictionary.") + .def("apply", - py::overload_cast(&qc::Permutation::apply, - py::const_), - "controls"_a) + nb::overload_cast(&qc::Permutation::apply, + nb::const_), + "controls"_a, R"pb(Apply the permutation to a set of controls. + +Args: + controls: The set of controls to apply the permutation to. + +Returns: + The set of controls with the permutation applied.)pb") + .def("apply", - py::overload_cast(&qc::Permutation::apply, - py::const_), - "targets"_a) - .def("clear", [](qc::Permutation& p) { p.clear(); }) - .def("__getitem__", - [](const qc::Permutation& p, const qc::Qubit q) { return p.at(q); }) - .def("__setitem__", [](qc::Permutation& p, const qc::Qubit q, - const qc::Qubit r) { p[q] = r; }) - .def("__delitem__", - [](qc::Permutation& p, const qc::Qubit q) { p.erase(q); }) - .def("__len__", &qc::Permutation::size) - .def("__iter__", - [](const qc::Permutation& p) { - return py::make_key_iterator(p.begin(), p.end()); - }) + nb::overload_cast(&qc::Permutation::apply, + nb::const_), + "targets"_a, R"pb(Apply the permutation to a list of targets. + +Args: + targets: The list of targets to apply the permutation to. + +Returns: + The list of targets with the permutation applied.)pb") + + .def( + "clear", [](qc::Permutation& p) { p.clear(); }, + "Clear the permutation of all indices and values.") + + .def( + "__getitem__", + [](const qc::Permutation& p, const nb::int_& index) { + const auto q = nbIntToQubit(index); + const auto it = p.find(q); + if (it == p.end()) { + const auto msg = + std::string("Permutation does not contain index ") + + std::to_string(q); + throw nb::key_error(msg.c_str()); + } + return it->second; + }, + "index"_a, R"pb(Get the value of the permutation at the given index. + +Args: + index: The index to get the value of the permutation at. + +Returns: + The value of the permutation at the given index.)pb") + + .def( + "__setitem__", + [](qc::Permutation& p, const nb::int_& index, const nb::int_& value) { + const auto q = nbIntToQubit(index); + const auto r = nbIntToQubit(value); + p[q] = r; + }, + "index"_a, "value"_a, + R"pb(Set the value of the permutation at the given index. + +Args: + index: The index to set the value of the permutation at. + value: The value to set the permutation at the given index to.)pb") + + .def( + "__delitem__", + [](qc::Permutation& p, const nb::int_& index) { + const auto q = nbIntToQubit(index); + const auto it = p.find(q); + if (it == p.end()) { + // Match Python's KeyError semantics for missing keys. + const auto msg = + std::string("Permutation does not contain index ") + + std::to_string(q); + throw nb::key_error(msg.c_str()); + } + p.erase(it); + }, + "index"_a, + R"pb(Delete the value of the permutation at the given index. + +Args: + index: The index to delete the value of the permutation at.)pb") + + .def("__len__", &qc::Permutation::size, + "Return the number of indices in the permutation.") + + .def( + "__iter__", + [](const qc::Permutation& p) { + return make_key_iterator( + nb::type(), "key_iterator", p.begin(), p.end(), + "Return an iterator over the indices of the permutation."); + }, + nb::keep_alive<0, 1>()) + .def( "items", [](const qc::Permutation& p) { - return py::make_iterator(p.begin(), p.end()); + return make_iterator( + nb::type(), "item_iterator", p.begin(), + p.end(), + "Return an iterable over the items of the permutation."); }, - py::keep_alive<0, 1>()) - .def(py::self == py::self) // NOLINT(misc-redundant-expression) - .def(py::self != py::self) // NOLINT(misc-redundant-expression) - .def(hash(py::self)) + nb::sig("def items(self) -> collections.abc.ItemsView[int, int]"), + nb::keep_alive<0, 1>()) + + .def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")) + .def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")) + .def("__str__", [](const qc::Permutation& p) { std::stringstream ss; @@ -97,7 +208,8 @@ void registerPermutation(py::module& m) { ss << "})"; return ss.str(); }); - py::implicitly_convertible(); + + nb::implicitly_convertible(); } } // namespace mqt diff --git a/bindings/ir/register_quantum_computation.cpp b/bindings/ir/register_quantum_computation.cpp index 902f5da612..e1a2b3f439 100644 --- a/bindings/ir/register_quantum_computation.cpp +++ b/bindings/ir/register_quantum_computation.cpp @@ -17,18 +17,20 @@ #include "ir/operations/Operation.hpp" #include "qasm3/Importer.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -// clang-format on - +#include +#include #include #include +#include #include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -36,61 +38,134 @@ namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; using DiffType = std::vector>::difference_type; using SizeType = std::vector>::size_type; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerQuantumComputation(py::module& m) { +void registerQuantumComputation(const nb::module_& m) { auto wrap = [](DiffType i, const SizeType size) { if (i < 0) { i += static_cast(size); } if (i < 0 || std::cmp_greater_equal(i, size)) { - throw py::index_error(); + throw nb::index_error(); } return i; }; - auto qc = py::class_(m, "QuantumComputation"); + auto qc = nb::class_( + m, "QuantumComputation", + nb::sig("class " + "QuantumComputation(collections.abc.MutableSequence[mqt.core.ir." + "operations.Operation])"), + R"pb(The main class for representing quantum computations within the MQT. + +Acts as mutable sequence of :class:`~mqt.core.ir.operations.Operation` objects, which represent the individual operations in the quantum computation. + +Args: + nq: The number of qubits in the quantum computation. + nc: The number of classical bits in the quantum computation. + seed: The seed to use for the internal random number generator.)pb"); ///--------------------------------------------------------------------------- /// \n Constructors \n ///--------------------------------------------------------------------------- - qc.def(py::init(), "nq"_a = 0U, + qc.def(nb::init(), "nq"_a = 0U, "nc"_a = 0U, "seed"_a = 0U); // expose the static constructor from qasm strings or files - qc.def_static("from_qasm_str", &qasm3::Importer::imports, "qasm"_a); - qc.def_static("from_qasm", &qasm3::Importer::importf, "filename"_a); + qc.def_static("from_qasm_str", &qasm3::Importer::imports, "qasm"_a, + R"pb(Create a QuantumComputation object from an OpenQASM string. + +Args: + qasm: The OpenQASM string to create the QuantumComputation object from. + +Returns: + The QuantumComputation object created from the OpenQASM string.)pb"); + + qc.def_static("from_qasm", &qasm3::Importer::importf, "filename"_a, + R"pb(Create a QuantumComputation object from an OpenQASM file. + +Args: + filename: The filename of the OpenQASM file to create the QuantumComputation object from. + +Returns: + The QuantumComputation object created from the OpenQASM file.)pb"); ///--------------------------------------------------------------------------- /// \n General Properties \n ///--------------------------------------------------------------------------- - qc.def_property("name", &qc::QuantumComputation::getName, - &qc::QuantumComputation::setName); - qc.def_property_readonly("num_qubits", &qc::QuantumComputation::getNqubits); - qc.def_property_readonly("num_ancilla_qubits", - &qc::QuantumComputation::getNancillae); - qc.def_property_readonly("num_garbage_qubits", - &qc::QuantumComputation::getNgarbageQubits); - qc.def_property_readonly("num_measured_qubits", - &qc::QuantumComputation::getNmeasuredQubits); - qc.def_property_readonly("num_data_qubits", - &qc::QuantumComputation::getNqubitsWithoutAncillae); - qc.def_property_readonly("num_classical_bits", - &qc::QuantumComputation::getNcbits); - qc.def_property_readonly("num_ops", &qc::QuantumComputation::getNops); - qc.def("num_single_qubit_ops", &qc::QuantumComputation::getNsingleQubitOps); - qc.def("num_total_ops", &qc::QuantumComputation::getNindividualOps); - qc.def("depth", &qc::QuantumComputation::getDepth); - qc.def_property("global_phase", &qc::QuantumComputation::getGlobalPhase, - &qc::QuantumComputation::gphase); - qc.def("invert", &qc::QuantumComputation::invert); - qc.def("to_operation", &qc::QuantumComputation::asOperation); + qc.def_prop_rw("name", &qc::QuantumComputation::getName, + &qc::QuantumComputation::setName, + "The name of the quantum computation."); + + qc.def_prop_ro("num_qubits", &qc::QuantumComputation::getNqubits, + "The total number of qubits in the quantum computation."); + + qc.def_prop_ro("num_ancilla_qubits", &qc::QuantumComputation::getNancillae, + R"pb(The number of ancilla qubits in the quantum computation. + +Note: + Ancilla qubits are qubits that always start in a fixed state (usually :math:`|0\\rangle`).)pb"); + + qc.def_prop_ro("num_garbage_qubits", + &qc::QuantumComputation::getNgarbageQubits, + R"pb(The number of garbage qubits in the quantum computation. + +Note: + Garbage qubits are qubits whose final state is not relevant for the computation.)pb"); + + qc.def_prop_ro( + "num_measured_qubits", &qc::QuantumComputation::getNmeasuredQubits, + R"pb(The number of qubits that are measured in the quantum computation. + +Computed as :math:`| \text{qubits} | - | \text{garbage} |`.)pb"); + + qc.def_prop_ro("num_data_qubits", + &qc::QuantumComputation::getNqubitsWithoutAncillae, + R"pb(The number of data qubits in the quantum computation. + +Computed as :math:`| \text{qubits} | - | \text{ancilla} |`.)pb"); + + qc.def_prop_ro("num_classical_bits", &qc::QuantumComputation::getNcbits, + "The number of classical bits in the quantum computation."); + + qc.def_prop_ro("num_ops", &qc::QuantumComputation::getNops, + "The number of operations in the quantum computation."); + + qc.def("num_single_qubit_ops", &qc::QuantumComputation::getNsingleQubitOps, + "Return the number of single-qubit operations in the quantum " + "computation."); + + qc.def("num_total_ops", &qc::QuantumComputation::getNindividualOps, + R"pb(Return the total number of operations in the quantum computation. + +Recursively counts sub-operations (e.g., from :class:`~mqt.core.ir.operations.CompoundOperation` objects).)pb"); + + qc.def("depth", &qc::QuantumComputation::getDepth, + "Return the depth of the quantum computation."); + + qc.def_prop_rw("global_phase", &qc::QuantumComputation::getGlobalPhase, + &qc::QuantumComputation::gphase, + "The global phase of the quantum computation."); + + qc.def("invert", &qc::QuantumComputation::invert, + "Invert the quantum computation in-place by inverting each operation " + "and reversing the order of operations."); + + qc.def("to_operation", &qc::QuantumComputation::asOperation, + R"pb(Convert the quantum computation to a single operation. + +This gives ownership of the operations to the resulting operation, so the quantum computation will be empty after this operation. + +When the quantum computation contains more than one operation, the resulting operation is a :class:`~mqt.core.ir.operations.CompoundOperation`. + +Returns: + The operation representing the quantum computation.)pb"); ///--------------------------------------------------------------------------- /// \n Mutable Sequence Interface \n @@ -98,161 +173,317 @@ void registerQuantumComputation(py::module& m) { qc.def( "__getitem__", - [&wrap](const qc::QuantumComputation& circ, DiffType i) { + [wrap](const qc::QuantumComputation& circ, DiffType i) { i = wrap(i, circ.getNops()); return circ.at(static_cast(i)).get(); }, - py::return_value_policy::reference_internal, "index"_a); + nb::rv_policy::reference_internal, "index"_a, + R"pb(Get the operation at the given index. + +Note: + This gives write access to the operation at the given index. + +Args: + index: The index of the operation to get. + +Returns: + The operation at the given index.)pb"); + qc.def( "__getitem__", - [](qc::QuantumComputation& circ, const py::slice& slice) { - std::size_t start{}; - std::size_t stop{}; - std::size_t step{}; - std::size_t sliceLength{}; - if (!slice.compute(circ.getNops(), &start, &stop, &step, - &sliceLength)) { - throw py::error_already_set(); - } + [](qc::QuantumComputation& circ, const nb::slice& slice) { + auto [start, stop, step, sliceLength] = slice.compute(circ.getNops()); auto ops = std::vector(); ops.reserve(sliceLength); - for (std::size_t i = start; i < stop; i += step) { - ops.emplace_back(circ.at(i).get()); + for (std::size_t i = 0; i < sliceLength; ++i) { + auto idx = + static_cast(start) + (static_cast(i) * step); + ops.emplace_back(circ.at(static_cast(idx)).get()); } return ops; }, - py::return_value_policy::reference_internal, "index"_a); + nb::rv_policy::reference_internal, "index"_a, + R"pb(Get a slice of operations from the quantum computation. + +Note: + This gives write access to the operations in the given slice. + +Args: + index: The slice of operations to get. + +Returns: + The operations in the given slice.)pb"); + qc.def( "__setitem__", - [&wrap](qc::QuantumComputation& circ, DiffType i, - const qc::Operation& op) { + [wrap](qc::QuantumComputation& circ, DiffType i, + const qc::Operation& op) { i = wrap(i, circ.getNops()); circ.at(static_cast(i)) = op.clone(); }, - "index"_a, "value"_a); + "index"_a, "value"_a, R"pb(Set the operation at the given index. + +Args: + index: The index of the operation to set. + value: The operation to set at the given index.)pb"); + qc.def( "__setitem__", - [](qc::QuantumComputation& circ, const py::slice& slice, + [](qc::QuantumComputation& circ, const nb::slice& slice, const std::vector& ops) { - std::size_t start{}; - std::size_t stop{}; - std::size_t step{}; - std::size_t sliceLength{}; - if (!slice.compute(circ.getNops(), &start, &stop, &step, - &sliceLength)) { - throw py::error_already_set(); - } + auto [start, stop, step, sliceLength] = slice.compute(circ.getNops()); if (sliceLength != ops.size()) { throw std::runtime_error( "Length of slice and number of operations do not match."); } for (std::size_t i = 0; i < sliceLength; ++i) { - circ.at(start) = ops[i]->clone(); + assert(ops[i] != nullptr && "ops must not contain nullptr"); + circ.at(static_cast(start)) = ops[i]->clone(); start += step; } }, - "index"_a, "value"_a); + nb::sig("def __setitem__(self, index: slice, value: " + "collections.abc.Iterable[mqt.core.ir.operations.Operation]) -> " + "None"), + R"pb(Set the operations in the given slice. + +Args: + index: The slice of operations to set. + value: The operations to set in the given slice.)pb"); + qc.def( "__delitem__", - [&wrap](qc::QuantumComputation& circ, DiffType i) { + [wrap](qc::QuantumComputation& circ, DiffType i) { i = wrap(i, circ.getNops()); circ.erase(circ.begin() + i); }, - "index"_a); + "index"_a, R"pb(Delete the operation at the given index. + +Args: + index: The index of the operation to delete.)pb"); + qc.def( "__delitem__", - [](qc::QuantumComputation& circ, const py::slice& slice) { - std::size_t start{}; - std::size_t stop{}; - std::size_t step{}; - std::size_t sliceLength{}; - if (!slice.compute(circ.getNops(), &start, &stop, &step, - &sliceLength)) { - throw py::error_already_set(); + [](qc::QuantumComputation& circ, const nb::slice& slice) { + auto [start, stop, step, sliceLength] = slice.compute(circ.getNops()); + // Delete in reverse order to not invalidate indices + std::vector indices; + indices.reserve(sliceLength); + for (std::size_t i = 0; i < sliceLength; ++i) { + indices.emplace_back(static_cast(start) + + (static_cast(i) * step)); } - // delete in reverse order to not invalidate indices - for (std::size_t i = sliceLength; i > 0; --i) { - circ.erase(circ.begin() + - static_cast(start + ((i - 1) * step))); + std::ranges::sort(indices, std::greater<>()); + for (const auto idx : indices) { + circ.erase(circ.begin() + idx); } }, - "index"_a); - qc.def("__len__", &qc::QuantumComputation::getNops); + "index"_a, R"pb(Delete the operations in the given slice. + +Args: + index: The slice of operations to delete.)pb"); + + qc.def("__len__", &qc::QuantumComputation::getNops, + "Return the number of operations in the quantum computation."); + qc.def( "insert", [](qc::QuantumComputation& circ, std::size_t idx, const qc::Operation& op) { circ.insert(circ.begin() + static_cast(idx), op.clone()); }, - "index"_a, "value"_a); + "index"_a, "value"_a, R"pb(Insert an operation at the given index. + +Args: + index: The index to insert the operation at. + value: The operation to insert.)pb"); + qc.def( "append", [](qc::QuantumComputation& circ, const qc::Operation& op) { circ.emplace_back(op.clone()); }, - "value"_a); - qc.def("reverse", &qc::QuantumComputation::reverse); - qc.def("clear", py::overload_cast<>(&qc::QuantumComputation::reset)); + "value"_a, R"pb(Append an operation to the end of the quantum computation. + +Args: + value: The operation to append.)pb"); + + qc.def("reverse", &qc::QuantumComputation::reverse, + "Reverse the order of the operations in the quantum computation " + "(in-place)."); + + qc.def("clear", nb::overload_cast<>(&qc::QuantumComputation::reset), + "Clear the quantum computation of all operations."); ///--------------------------------------------------------------------------- /// \n (Qu)Bit Registers \n ///--------------------------------------------------------------------------- qc.def("add_qubit_register", &qc::QuantumComputation::addQubitRegister, "n"_a, - "name"_a = "q"); + "name"_a = "q", R"pb(Add a qubit register to the quantum computation. + +Args: + n: The number of qubits in the qubit register. + name: The name of the qubit register. + +Returns: + The qubit register added to the quantum computation.)pb"); + qc.def("add_classical_register", - &qc::QuantumComputation::addClassicalRegister, "n"_a, "name"_a = "c"); + &qc::QuantumComputation::addClassicalRegister, "n"_a, "name"_a = "c", + R"pb(Add a classical register to the quantum computation. + +Args: + n: The number of bits in the classical register. + name: The name of the classical register. + +Returns: + The classical register added to the quantum computation.)pb"); + qc.def("add_ancillary_register", - &qc::QuantumComputation::addAncillaryRegister, "n"_a, - "name"_a = "anc"); + &qc::QuantumComputation::addAncillaryRegister, "n"_a, "name"_a = "anc", + R"pb(Add an ancillary register to the quantum computation. + +Args: + n: The number of qubits in the ancillary register. + name: The name of the ancillary register. + +Returns: + The ancillary register added to the quantum computation.)pb"); + qc.def("unify_quantum_registers", - &qc::QuantumComputation::unifyQuantumRegisters, "name"_a = "q"); + &qc::QuantumComputation::unifyQuantumRegisters, "name"_a = "q", + R"pb(Unify all quantum registers in the quantum computation. + +Args: + name: The name of the unified quantum register. + +Returns: + The unified quantum register.)pb"); + + qc.def_prop_ro("qregs", &qc::QuantumComputation::getQuantumRegisters, + "The quantum registers in the quantum computation."); - qc.def_property_readonly("qregs", - &qc::QuantumComputation::getQuantumRegisters); - qc.def_property_readonly("cregs", - &qc::QuantumComputation::getClassicalRegisters); - qc.def_property_readonly("ancregs", - &qc::QuantumComputation::getAncillaRegisters); + qc.def_prop_ro("cregs", &qc::QuantumComputation::getClassicalRegisters, + "The classical registers in the quantum computation."); + + qc.def_prop_ro("ancregs", &qc::QuantumComputation::getAncillaRegisters, + "The ancillary registers in the quantum computation."); ///--------------------------------------------------------------------------- /// \n Input Layout and Output Permutation \n ///--------------------------------------------------------------------------- - qc.def_readwrite("initial_layout", &qc::QuantumComputation::initialLayout); - qc.def_readwrite("output_permutation", - &qc::QuantumComputation::outputPermutation); - qc.def("initialize_io_mapping", &qc::QuantumComputation::initializeIOMapping); + qc.def_rw("initial_layout", &qc::QuantumComputation::initialLayout, + R"pb(The initial layout of the qubits in the quantum computation. + +This is a permutation of the qubits in the quantum computation. +It is mainly used to track the mapping of circuit qubits to device qubits during quantum circuit compilation. +The keys are the device qubits (in which a compiled circuit is expressed in), and the values are the circuit qubits (in which the original quantum circuit is expressed in). + +Any operations in the quantum circuit are expected to be expressed in terms of the keys of the initial layout. + +Examples: + - If no initial layout is explicitly specified (which is the default), the initial layout is assumed to be the identity permutation. + - Assume a three-qubit circuit has been compiled to a four qubit device and circuit qubit 0 is mapped to device qubit 1, circuit qubit 1 is mapped to device qubit 2, and circuit qubit 2 is mapped to device qubit 3. + Then the initial layout is {1: 0, 2: 1, 3: 2}.)pb"); + + qc.def_rw( + "output_permutation", &qc::QuantumComputation::outputPermutation, + R"pb(The output permutation of the qubits in the quantum computation. + +This is a permutation of the qubits in the quantum computation. +It is mainly used to track where individual qubits end up at the end of the quantum computation, for example after a circuit has been compiled to a specific device and SWAP gates have been inserted, which permute the qubits. +Similar to the initial layout, the keys are the qubits in the circuit and the values are the qubits in the "original" circuit. + +Examples: + - If no output permutation is explicitly specified and the circuit does not contain measurements at the end, the output permutation is assumed to be the identity permutation. + - If the circuit contains measurements at the end, these measurements are used to infer the output permutation. + Assume a three-qubit circuit has been compiled to a four qubit device and, at the end of the circuit, circuit qubit 0 is measured into classical bit 2, circuit qubit 1 is measured into classical bit 1, and circuit qubit 3 is measured into classical bit 0. + Then the output permutation is {0: 2, 1: 1, 3: 0}.)pb"); + + qc.def("initialize_io_mapping", &qc::QuantumComputation::initializeIOMapping, + R"pb(Initialize the I/O mapping of the quantum computation. + +If no initial layout is explicitly specified, the initial layout is assumed to be the identity permutation. +If the circuit contains measurements at the end, these measurements are used to infer the output permutation.)pb"); ///--------------------------------------------------------------------------- /// \n Ancillary and Garbage Handling \n ///--------------------------------------------------------------------------- - qc.def_property_readonly( - "ancillary", py::overload_cast<>(&qc::QuantumComputation::getAncillary)); + qc.def_prop_ro( + "ancillary", nb::overload_cast<>(&qc::QuantumComputation::getAncillary), + "A list of booleans indicating whether each qubit is ancillary."); + qc.def("set_circuit_qubit_ancillary", - &qc::QuantumComputation::setLogicalQubitAncillary, "q"_a); - qc.def("se_circuit_qubits_ancillary", + &qc::QuantumComputation::setLogicalQubitAncillary, "q"_a, + R"pb(Set a circuit (i.e., logical) qubit to be ancillary. + +Args: + q: The index of the circuit qubit to set as ancillary.)pb"); + + qc.def("set_circuit_qubits_ancillary", &qc::QuantumComputation::setLogicalQubitsAncillary, "q_min"_a, - "q_max"_a); + "q_max"_a, + R"pb(Set a range of circuit (i.e., logical) qubits to be ancillary. + +Args: + q_min: The minimum index of the circuit qubits to set as ancillary. + q_max: The maximum index of the circuit qubits to set as ancillary.)pb"); + qc.def("is_circuit_qubit_ancillary", - &qc::QuantumComputation::logicalQubitIsAncillary, "q"_a); - qc.def_property_readonly( - "garbage", py::overload_cast<>(&qc::QuantumComputation::getGarbage)); + &qc::QuantumComputation::logicalQubitIsAncillary, "q"_a, + R"pb(Check if a circuit (i.e., logical) qubit is ancillary. + +Args: + q: The index of the circuit qubit to check. + +Returns: + True if the circuit qubit is ancillary, False otherwise.)pb"); + + qc.def_prop_ro( + "garbage", nb::overload_cast<>(&qc::QuantumComputation::getGarbage), + "A list of booleans indicating whether each qubit is garbage."); + qc.def("set_circuit_qubit_garbage", - &qc::QuantumComputation::setLogicalQubitGarbage, "q"_a); + &qc::QuantumComputation::setLogicalQubitGarbage, "q"_a, + R"pb(Set a circuit (i.e., logical) qubit to be garbage. + +Args: + q: The index of the circuit qubit to set as garbage.)pb"); + qc.def("set_circuit_qubits_garbage", - &qc::QuantumComputation::setLogicalQubitsGarbage, "q_min"_a, - "q_max"_a); + &qc::QuantumComputation::setLogicalQubitsGarbage, "q_min"_a, "q_max"_a, + R"pb(Set a range of circuit (i.e., logical) qubits to be garbage. + +Args: + q_min: The minimum index of the circuit qubits to set as garbage. + q_max: The maximum index of the circuit qubits to set as garbage.)pb"); + qc.def("is_circuit_qubit_garbage", - &qc::QuantumComputation::logicalQubitIsGarbage, "q"_a); + &qc::QuantumComputation::logicalQubitIsGarbage, "q"_a, + R"pb(Check if a circuit (i.e., logical) qubit is garbage. + +Args: + q: The index of the circuit qubit to check. + +Returns: + True if the circuit qubit is garbage, False otherwise.)pb"); ///--------------------------------------------------------------------------- /// \n Symbolic Circuit Handling \n ///--------------------------------------------------------------------------- - qc.def_property_readonly("variables", &qc::QuantumComputation::getVariables); - qc.def("add_variable", &qc::QuantumComputation::addVariable, "var"_a); + qc.def_prop_ro("variables", &qc::QuantumComputation::getVariables, + "The set of variables in the quantum computation."); + + qc.def("add_variable", &qc::QuantumComputation::addVariable, "var"_a, + R"pb(Add a variable to the quantum computation. + +Args: + var: The variable to add.)pb"); + qc.def( "add_variables", [](qc::QuantumComputation& circ, @@ -261,37 +492,97 @@ void registerQuantumComputation(py::module& m) { circ.addVariable(var); } }, - "vars_"_a); - qc.def("is_variable_free", &qc::QuantumComputation::isVariableFree); - qc.def("instantiate", &qc::QuantumComputation::instantiate, "assignment"_a); - qc.def("instantiate_inplace", &qc::QuantumComputation::instantiateInplace, - "assignment"_a); + "vars_"_a, R"pb(Add multiple variables to the quantum computation. + +Args: + vars_: The variables to add.)pb"); + + qc.def("is_variable_free", &qc::QuantumComputation::isVariableFree, + R"pb(Check if the quantum computation is free of variables. + +Returns: + True if the quantum computation is free of variables, False otherwise.)pb"); + + qc.def( + "instantiate", &qc::QuantumComputation::instantiate, "assignment"_a, + R"pb(Instantiate the quantum computation with the given variable assignment. + +Args: + assignment: The variable assignment to instantiate the quantum computation with. + +Returns: + The instantiated quantum computation.)pb"); + + qc.def( + "instantiate_inplace", &qc::QuantumComputation::instantiateInplace, + "assignment"_a, + R"pb(Instantiate the quantum computation with the given variable assignment in-place. + +Args: + assignment: The variable assignment to instantiate the quantum computation with.)pb"); ///--------------------------------------------------------------------------- /// \n Output Handling \n ///--------------------------------------------------------------------------- - qc.def("qasm2_str", - [](const qc::QuantumComputation& circ) { return circ.toQASM(false); }); + qc.def( + "qasm2_str", + [](const qc::QuantumComputation& circ) { return circ.toQASM(false); }, + R"pb(Return the OpenQASM2 representation of the quantum computation as a string. + +Note: + This uses some custom extensions to OpenQASM 2.0 that allow for easier definition of multi-controlled gates. + These extensions might not be supported by all OpenQASM 2.0 parsers. + Consider using the :meth:`qasm3_str` method instead, which uses OpenQASM 3.0 that natively supports multi-controlled gates. + The export also assumes the bigger, non-standard `qelib1.inc` from Qiskit is available. + +Returns: + The OpenQASM2 representation of the quantum computation as a string.)pb"); + qc.def( "qasm2", [](const qc::QuantumComputation& circ, const std::string& filename) { circ.dump(filename, qc::Format::OpenQASM2); }, - "filename"_a); - qc.def("qasm3_str", - [](const qc::QuantumComputation& circ) { return circ.toQASM(true); }); + "filename"_a, + nb::sig("def qasm2(self, filename: os.PathLike[str] | str) -> None"), + R"pb(Write the OpenQASM2 representation of the quantum computation to a file. + +See Also: + :meth:`qasm2_str` + +Args: + filename: The filename of the file to write the OpenQASM2 representation to.)pb"); + + qc.def( + "qasm3_str", + [](const qc::QuantumComputation& circ) { return circ.toQASM(true); }, + R"pb(Return the OpenQASM3 representation of the quantum computation as a string. + +Returns: + The OpenQASM3 representation of the quantum computation as a string.)pb"); + qc.def( "qasm3", [](const qc::QuantumComputation& circ, const std::string& filename) { circ.dump(filename, qc::Format::OpenQASM3); }, - "filename"_a); + "filename"_a, + nb::sig("def qasm3(self, filename: os.PathLike[str] | str) -> None"), + R"pb(Write the OpenQASM3 representation of the quantum computation to a file. + +See Also: + :meth:`qasm3_str` + +Args: + filename: The filename of the file to write the OpenQASM3 representation to.)pb"); + qc.def("__str__", [](const qc::QuantumComputation& circ) { auto ss = std::stringstream(); circ.print(ss); return ss.str(); }); + qc.def("__repr__", [](const qc::QuantumComputation& circ) { auto ss = std::stringstream(); ss << "QuantumComputation(num_qubits=" << circ.getNqubits() @@ -305,131 +596,1453 @@ void registerQuantumComputation(py::module& m) { /// \n Operations \n ///--------------------------------------------------------------------------- -#define DEFINE_SINGLE_TARGET_OPERATION(op) \ - qc.def(#op, &qc::QuantumComputation::op, "q"_a); \ - qc.def("c" #op, &qc::QuantumComputation::c##op, "control"_a, "target"_a); \ - qc.def("mc" #op, &qc::QuantumComputation::mc##op, "controls"_a, "target"_a); - - DEFINE_SINGLE_TARGET_OPERATION(i) - DEFINE_SINGLE_TARGET_OPERATION(x) - DEFINE_SINGLE_TARGET_OPERATION(y) - DEFINE_SINGLE_TARGET_OPERATION(z) - DEFINE_SINGLE_TARGET_OPERATION(h) - DEFINE_SINGLE_TARGET_OPERATION(s) - DEFINE_SINGLE_TARGET_OPERATION(sdg) - DEFINE_SINGLE_TARGET_OPERATION(t) - DEFINE_SINGLE_TARGET_OPERATION(tdg) - DEFINE_SINGLE_TARGET_OPERATION(v) - DEFINE_SINGLE_TARGET_OPERATION(vdg) - DEFINE_SINGLE_TARGET_OPERATION(sx) - DEFINE_SINGLE_TARGET_OPERATION(sxdg) - -#define DEFINE_SINGLE_TARGET_SINGLE_PARAMETER_OPERATION(op, param) \ - qc.def(#op, &qc::QuantumComputation::op, py::arg(#param), "q"_a); \ - qc.def("c" #op, &qc::QuantumComputation::c##op, py::arg(#param), \ - "control"_a, "target"_a); \ - qc.def("mc" #op, &qc::QuantumComputation::mc##op, py::arg(#param), \ - "controls"_a, "target"_a); - - DEFINE_SINGLE_TARGET_SINGLE_PARAMETER_OPERATION(rx, theta) - DEFINE_SINGLE_TARGET_SINGLE_PARAMETER_OPERATION(ry, theta) - DEFINE_SINGLE_TARGET_SINGLE_PARAMETER_OPERATION(rz, theta) - DEFINE_SINGLE_TARGET_SINGLE_PARAMETER_OPERATION(p, theta) - -#define DEFINE_SINGLE_TARGET_TWO_PARAMETER_OPERATION(op, param0, param1) \ - qc.def(#op, &qc::QuantumComputation::op, py::arg(#param0), py::arg(#param1), \ - "q"_a); \ - qc.def("c" #op, &qc::QuantumComputation::c##op, py::arg(#param0), \ - py::arg(#param1), "control"_a, "target"_a); \ - qc.def("mc" #op, &qc::QuantumComputation::mc##op, py::arg(#param0), \ - py::arg(#param1), "controls"_a, "target"_a); - - DEFINE_SINGLE_TARGET_TWO_PARAMETER_OPERATION(u2, phi, lambda_) - DEFINE_SINGLE_TARGET_TWO_PARAMETER_OPERATION(r, theta, phi) - -#define DEFINE_SINGLE_TARGET_THREE_PARAMETER_OPERATION(op, param0, param1, \ - param2) \ - qc.def(#op, &qc::QuantumComputation::op, py::arg(#param0), py::arg(#param1), \ - py::arg(#param2), "q"_a); \ - qc.def("c" #op, &qc::QuantumComputation::c##op, py::arg(#param0), \ - py::arg(#param1), py::arg(#param2), "control"_a, "target"_a); \ - qc.def("mc" #op, &qc::QuantumComputation::mc##op, py::arg(#param0), \ - py::arg(#param1), py::arg(#param2), "controls"_a, "target"_a); - - DEFINE_SINGLE_TARGET_THREE_PARAMETER_OPERATION(u, theta, phi, lambda_) - -#define DEFINE_TWO_TARGET_OPERATION(op) \ - qc.def(#op, &qc::QuantumComputation::op, "target1"_a, "target2"_a); \ - qc.def("c" #op, &qc::QuantumComputation::c##op, "control"_a, "target1"_a, \ - "target2"_a); \ - qc.def("mc" #op, &qc::QuantumComputation::mc##op, "controls"_a, "target1"_a, \ - "target2"_a); - - DEFINE_TWO_TARGET_OPERATION(swap) - DEFINE_TWO_TARGET_OPERATION(dcx) - DEFINE_TWO_TARGET_OPERATION(ecr) - DEFINE_TWO_TARGET_OPERATION(iswap) - DEFINE_TWO_TARGET_OPERATION(peres) - DEFINE_TWO_TARGET_OPERATION(peresdg) - -#define DEFINE_TWO_TARGET_SINGLE_PARAMETER_OPERATION(op, param) \ - qc.def(#op, &qc::QuantumComputation::op, py::arg(#param), "target1"_a, \ - "target2"_a); \ - qc.def("c" #op, &qc::QuantumComputation::c##op, py::arg(#param), \ - "control"_a, "target1"_a, "target2"_a); \ - qc.def("mc" #op, &qc::QuantumComputation::mc##op, py::arg(#param), \ - "controls"_a, "target1"_a, "target2"_a); - - DEFINE_TWO_TARGET_SINGLE_PARAMETER_OPERATION(rxx, theta) - DEFINE_TWO_TARGET_SINGLE_PARAMETER_OPERATION(ryy, theta) - DEFINE_TWO_TARGET_SINGLE_PARAMETER_OPERATION(rzz, theta) - DEFINE_TWO_TARGET_SINGLE_PARAMETER_OPERATION(rzx, theta) - -#define DEFINE_TWO_TARGET_TWO_PARAMETER_OPERATION(op, param0, param1) \ - qc.def(#op, &qc::QuantumComputation::op, py::arg(#param0), py::arg(#param1), \ - "target1"_a, "target2"_a); \ - qc.def("c" #op, &qc::QuantumComputation::c##op, py::arg(#param0), \ - py::arg(#param1), "control"_a, "target1"_a, "target2"_a); \ - qc.def("mc" #op, &qc::QuantumComputation::mc##op, py::arg(#param0), \ - py::arg(#param1), "controls"_a, "target1"_a, "target2"_a); - - DEFINE_TWO_TARGET_TWO_PARAMETER_OPERATION(xx_minus_yy, theta, beta) - DEFINE_TWO_TARGET_TWO_PARAMETER_OPERATION(xx_plus_yy, theta, beta) - -#undef DEFINE_SINGLE_TARGET_OPERATION -#undef DEFINE_SINGLE_TARGET_SINGLE_PARAMETER_OPERATION -#undef DEFINE_SINGLE_TARGET_TWO_PARAMETER_OPERATION -#undef DEFINE_SINGLE_TARGET_THREE_PARAMETER_OPERATION -#undef DEFINE_TWO_TARGET_OPERATION -#undef DEFINE_TWO_TARGET_SINGLE_PARAMETER_OPERATION -#undef DEFINE_TWO_TARGET_TWO_PARAMETER_OPERATION - - qc.def("gphase", &qc::QuantumComputation::gphase, "phase"_a); + // I + + qc.def("i", &qc::QuantumComputation::i, "q"_a, + R"pb(Apply an identity operation. + +.. math:: + I = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("ci", &qc::QuantumComputation::ci, "control"_a, "target"_a, + nb::sig("def ci(self, control: mqt.core.ir.operations.Control | int, " + "target: int) " + "-> None"), + R"pb(Apply a controlled identity operation. + +See Also: + :meth:`i` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mci", &qc::QuantumComputation::mci, "controls"_a, "target"_a, + nb::sig("def mci(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled identity operation. + +See Also: + :meth:`i` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // X + + qc.def("x", &qc::QuantumComputation::x, "q"_a, + R"pb(Apply a Pauli-X gate. + +.. math:: + X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("cx", &qc::QuantumComputation::cx, "control"_a, "target"_a, + nb::sig("def cx(self, control: mqt.core.ir.operations.Control | int, " + "target: int) " + "-> None"), + R"pb(Apply a controlled Pauli-X (i.e., CNOT or CX) gate. + +See Also: + :meth:`x` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcx", &qc::QuantumComputation::mcx, "controls"_a, "target"_a, + nb::sig("def mcx(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled Pauli-X (i.e., Toffoli or MCX) gate. + +See Also: + :meth:`x` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // Y + + qc.def("y", &qc::QuantumComputation::y, "q"_a, + R"pb(Apply a Pauli-Y gate. + +.. math:: + Y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("cy", &qc::QuantumComputation::cy, "control"_a, "target"_a, + nb::sig("def cy(self, control: mqt.core.ir.operations.Control | int, " + "target: int) " + "-> None"), + R"pb(Apply a controlled Pauli-Y gate. + +See Also: + :meth:`y` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcy", &qc::QuantumComputation::mcy, "controls"_a, "target"_a, + nb::sig("def mcy(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled Pauli-Y gate. + +See Also: + :meth:`y` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // Z + + qc.def("z", &qc::QuantumComputation::z, "q"_a, + R"pb(Apply a Pauli-Z gate. + +.. math:: + Z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("cz", &qc::QuantumComputation::cz, "control"_a, "target"_a, + nb::sig("def cz(self, control: mqt.core.ir.operations.Control | int, " + "target: int) " + "-> None"), + R"pb(Apply a controlled Pauli-Z gate. + +See Also: + :meth:`z` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcz", &qc::QuantumComputation::mcz, "controls"_a, "target"_a, + nb::sig("def mcz(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled Pauli-Z gate. + +See Also: + :meth:`z` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // H + + qc.def("h", &qc::QuantumComputation::h, "q"_a, + R"pb(Apply a Hadamard gate. + +.. math:: + H = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("ch", &qc::QuantumComputation::ch, "control"_a, "target"_a, + nb::sig("def ch(self, control: mqt.core.ir.operations.Control | int, " + "target: int) " + "-> None"), + R"pb(Apply a controlled Hadamard gate. + +See Also: + :meth:`h` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mch", &qc::QuantumComputation::mch, "controls"_a, "target"_a, + nb::sig("def mch(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled Hadamard gate. + +See Also: + :meth:`h` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // S + + qc.def("s", &qc::QuantumComputation::s, "q"_a, + R"pb(Apply an S (i.e., phase) gate. + +.. math:: + S = \begin{pmatrix} 1 & 0 \\ 0 & i \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("cs", &qc::QuantumComputation::cs, "control"_a, "target"_a, + nb::sig("def cs(self, control: mqt.core.ir.operations.Control | int, " + "target: int) " + "-> None"), + R"pb(Apply a controlled S gate. + +See Also: + :meth:`s` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcs", &qc::QuantumComputation::mcs, "controls"_a, "target"_a, + nb::sig("def mcs(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled S gate. + +See Also: + :meth:`s` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // Sdg + + qc.def("sdg", &qc::QuantumComputation::sdg, "q"_a, + R"pb(Apply an :math:`S^\dagger` gate. + +.. math:: + S^\dagger = \begin{pmatrix} 1 & 0 \\ 0 & -i \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("csdg", &qc::QuantumComputation::csdg, "control"_a, "target"_a, + nb::sig("def csdg(self, control: mqt.core.ir.operations.Control | " + "int, target: " + "int) -> None"), + R"pb(Apply a controlled :math:`S^\dagger` gate. + +See Also: + :meth:`sdg` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcsdg", &qc::QuantumComputation::mcsdg, "controls"_a, "target"_a, + nb::sig("def mcsdg(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled :math:`S^\dagger` gate. + +See Also: + :meth:`sdg` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // T + + qc.def("t", &qc::QuantumComputation::t, "q"_a, + R"pb(Apply a T gate. + +.. math:: + T = \begin{pmatrix} 1 & 0 \\ 0 & e^{i \pi / 4} \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("ct", &qc::QuantumComputation::ct, "control"_a, "target"_a, + nb::sig("def ct(self, control: mqt.core.ir.operations.Control | int, " + "target: int) " + "-> None"), + R"pb(Apply a controlled T gate. + +See Also: + :meth:`t` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mct", &qc::QuantumComputation::mct, "controls"_a, "target"_a, + nb::sig("def mct(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled T gate. + +See Also: + :meth:`t` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // Tdg + + qc.def("tdg", &qc::QuantumComputation::tdg, "q"_a, + R"pb(Apply a :math:`T^\dagger` gate. + +.. math:: + T^\dagger = \begin{pmatrix} 1 & 0 \\ 0 & e^{-i \pi / 4} \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("ctdg", &qc::QuantumComputation::ctdg, "control"_a, "target"_a, + nb::sig("def ctdg(self, control: mqt.core.ir.operations.Control | " + "int, target: " + "int) -> None"), + R"pb(Apply a controlled :math:`T^\dagger` gate. + +See Also: + :meth:`tdg` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mctdg", &qc::QuantumComputation::mctdg, "controls"_a, "target"_a, + nb::sig("def mctdg(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled :math:`T^\dagger` gate. + +See Also: + :meth:`tdg` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // V + + qc.def("v", &qc::QuantumComputation::v, "q"_a, + R"pb(Apply a V gate. + +.. math:: + V = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & -i \\ -i & 1 \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("cv", &qc::QuantumComputation::cv, "control"_a, "target"_a, + nb::sig("def cv(self, control: mqt.core.ir.operations.Control | int, " + "target: int) " + "-> None"), + R"pb(Apply a controlled V gate. + +See Also: + :meth:`v` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcv", &qc::QuantumComputation::mcv, "controls"_a, "target"_a, + nb::sig("def mcv(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled V gate. + +See Also: + :meth:`v` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // Vdg + + qc.def("vdg", &qc::QuantumComputation::vdg, "q"_a, + R"pb(Apply a :math:`V^\dagger` gate. + +.. math:: + V^\dagger = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & i \\ i & 1 \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("cvdg", &qc::QuantumComputation::cvdg, "control"_a, "target"_a, + nb::sig("def cvdg(self, control: mqt.core.ir.operations.Control | " + "int, target: " + "int) -> None"), + R"pb(Apply a controlled :math:`V^\dagger` gate. + +See Also: + :meth:`vdg` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcvdg", &qc::QuantumComputation::mcvdg, "controls"_a, "target"_a, + nb::sig("def mcvdg(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled :math:`V^\dagger` gate. + +See Also: + :meth:`vdg` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // SX + + qc.def("sx", &qc::QuantumComputation::sx, "q"_a, + R"pb(Apply a :math:`\sqrt{X}` gate. + +.. math:: + \sqrt{X} = \frac{1}{2} \begin{pmatrix} 1 + i & 1 - i \\ 1 - i & 1 + i \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("csx", &qc::QuantumComputation::csx, "control"_a, "target"_a, + nb::sig("def csx(self, control: mqt.core.ir.operations.Control | int, " + "target: " + "int) -> None"), + R"pb(Apply a controlled :math:`\sqrt{X}` gate. + +See Also: + :meth:`sx` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcsx", &qc::QuantumComputation::mcsx, "controls"_a, "target"_a, + nb::sig("def mcsx(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled :math:`\sqrt{X}` gate. + +See Also: + :meth:`sx` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // SXdg + + qc.def("sxdg", &qc::QuantumComputation::sxdg, "q"_a, + R"pb(Apply a :math:`\sqrt{X}^\dagger` gate. + +.. math:: + \sqrt{X}^{\dagger} = \frac{1}{2} \begin{pmatrix} 1 - i & 1 + i \\ 1 + i & 1 - i \end{pmatrix} + +Args: + q: The target qubit)pb"); + qc.def("csxdg", &qc::QuantumComputation::csxdg, "control"_a, "target"_a, + nb::sig("def csxdg(self, control: mqt.core.ir.operations.Control | " + "int, target: " + "int) -> None"), + R"pb(Apply a controlled :math:`\sqrt{X}^\dagger` gate. + +See Also: + :meth:`sxdg` + +Args: + control: The control qubit + target: The target qubit)pb"); + qc.def("mcsxdg", &qc::QuantumComputation::mcsxdg, "controls"_a, "target"_a, + nb::sig("def mcsxdg(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control " + "| int], target: int) -> None"), + R"pb(Apply a multi-controlled :math:`\sqrt{X}^\dagger` gate. + +See Also: + :meth:`sxdg` + +Args: + controls: The control qubits + target: The target qubit)pb"); + + // RX + + qc.def("rx", &qc::QuantumComputation::rx, "theta"_a, "q"_a, + R"pb(Apply an :math:`R_x(\theta)` gate. + +.. math:: + R_x(\theta) = e^{-i \theta X / 2} = \cos(\theta / 2) I - i \sin(\theta / 2) X + = \begin{pmatrix} \cos(\theta / 2) & -i \sin(\theta / 2) \\ -i \sin(\theta / 2) & \cos(\theta / 2) \end{pmatrix} + +Args: + theta: The rotation angle + q: The target qubit)pb"); + qc.def("crx", &qc::QuantumComputation::crx, "theta"_a, "control"_a, + "target"_a, + nb::sig("def crx(self, theta: mqt.core.ir.symbolic.Expression | " + "float, control: " + "mqt.core.ir.operations.Control | int, target: int) -> None"), + R"pb(Apply a controlled :math:`R_x(\theta)` gate. + +See Also: + :meth:`rx` + +Args: + theta: The rotation angle + control: The control qubit + target: The target qubit)pb"); + qc.def("mcrx", &qc::QuantumComputation::mcrx, "theta"_a, "controls"_a, + "target"_a, + nb::sig("def mcrx(self, theta: mqt.core.ir.symbolic.Expression | " + "float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target: int) " + "-> None"), + R"pb(Apply a multi-controlled :math:`R_x(\theta)` gate. + +See Also: + :meth:`rx` + +Args: + theta: The rotation angle + controls: The control qubits + target: The target qubit)pb"); + + // RY + + qc.def("ry", &qc::QuantumComputation::ry, "theta"_a, "q"_a, + R"pb(Apply an :math:`R_y(\theta)` gate. + +.. math:: + R_y(\theta) = e^{-i \theta Y / 2} = \cos(\theta / 2) I - i \sin(\theta / 2) Y + = \begin{pmatrix} \cos(\theta / 2) & -\sin(\theta / 2) \\ \sin(\theta / 2) & \cos(\theta / 2) \end{pmatrix} + +Args: + theta: The rotation angle + q: The target qubit)pb"); + qc.def("cry", &qc::QuantumComputation::cry, "theta"_a, "control"_a, + "target"_a, + nb::sig("def cry(self, theta: mqt.core.ir.symbolic.Expression | " + "float, control: " + "mqt.core.ir.operations.Control | int, target: int) -> None"), + R"pb(Apply a controlled :math:`R_y(\theta)` gate. + +See Also: + :meth:`ry` + +Args: + theta: The rotation angle + control: The control qubit + target: The target qubit)pb"); + qc.def("mcry", &qc::QuantumComputation::mcry, "theta"_a, "controls"_a, + "target"_a, + nb::sig("def mcry(self, theta: mqt.core.ir.symbolic.Expression | " + "float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target: int) " + "-> None"), + R"pb(Apply a multi-controlled :math:`R_y(\theta)` gate. + +See Also: + :meth:`ry` + +Args: + theta: The rotation angle + controls: The control qubits + target: The target qubit)pb"); + + // RZ + + qc.def("rz", &qc::QuantumComputation::rz, "theta"_a, "q"_a, + R"pb(Apply an :math:`R_z(\theta)` gate. + +.. math:: + R_z(\theta) = e^{-i \theta Z / 2} = \begin{pmatrix} e^{-i \theta / 2} & 0 \\ 0 & e^{i \theta / 2} \end{pmatrix} + +Args: + theta: The rotation angle + q: The target qubit)pb"); + qc.def("crz", &qc::QuantumComputation::crz, "theta"_a, "control"_a, + "target"_a, + nb::sig("def crz(self, theta: mqt.core.ir.symbolic.Expression | " + "float, control: " + "mqt.core.ir.operations.Control | int, target: int) -> None"), + R"pb(Apply a controlled :math:`R_z(\theta)` gate. + +See Also: + :meth:`rz` + +Args: + theta: The rotation angle + control: The control qubit + target: The target qubit)pb"); + qc.def("mcrz", &qc::QuantumComputation::mcrz, "theta"_a, "controls"_a, + "target"_a, + nb::sig("def mcrz(self, theta: mqt.core.ir.symbolic.Expression | " + "float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target: int) " + "-> None"), + R"pb(Apply a multi-controlled :math:`R_z(\theta)` gate. + +See Also: + :meth:`rz` + +Args: + theta: The rotation angle + controls: The control qubits + target: The target qubit)pb"); + + // P + + qc.def("p", &qc::QuantumComputation::p, "theta"_a, "q"_a, + R"pb(Apply a phase gate. + +.. math:: + P(\theta) = \begin{pmatrix} 1 & 0 \\ 0 & e^{i \theta} \end{pmatrix} + +Args: + theta: The rotation angle + q: The target qubit)pb"); + qc.def("cp", &qc::QuantumComputation::cp, "theta"_a, "control"_a, "target"_a, + nb::sig("def cp(self, theta: mqt.core.ir.symbolic.Expression | float, " + "control: " + "mqt.core.ir.operations.Control | int, target: int) -> None"), + R"pb(Apply a controlled phase gate. + +See Also: + :meth:`p` + +Args: + theta: The rotation angle + control: The control qubit + target: The target qubit)pb"); + qc.def("mcp", &qc::QuantumComputation::mcp, "theta"_a, "controls"_a, + "target"_a, + nb::sig("def mcp(self, theta: mqt.core.ir.symbolic.Expression | " + "float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target: int) " + "-> None"), + R"pb(Apply a multi-controlled phase gate. + +See Also: + :meth:`p` + +Args: + theta: The rotation angle + controls: The control qubits + target: The target qubit)pb"); + + // U2 + + qc.def("u2", &qc::QuantumComputation::u2, "phi"_a, "lambda_"_a, "q"_a, + R"pb(Apply a :math:`U_2(\phi, \lambda)` gate. + +.. math:: + U_2(\phi, \lambda) = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & -e^{i \lambda} \\ e^{i \phi} & e^{i (\phi + \lambda)} \end{pmatrix} + +Args: + phi: The rotation angle + lambda_: The rotation angle + q: The target qubit)pb"); + qc.def("cu2", &qc::QuantumComputation::cu2, "phi"_a, "lambda_"_a, "control"_a, + "target"_a, + nb::sig("def cu2(self, phi: mqt.core.ir.symbolic.Expression | float, " + "lambda_: " + "mqt.core.ir.symbolic.Expression | float, control: " + "mqt.core.ir.operations.Control | " + "int, target: int) -> None"), + R"pb(Apply a controlled :math:`U_2(\phi, \lambda)` gate. + +See Also: + :meth:`u2` + +Args: + phi: The rotation angle + lambda_: The rotation angle + control: The control qubit + target: The target qubit)pb"); + qc.def("mcu2", &qc::QuantumComputation::mcu2, "phi"_a, "lambda_"_a, + "controls"_a, "target"_a, + nb::sig("def mcu2(self, phi: mqt.core.ir.symbolic.Expression | float, " + "lambda_: " + "mqt.core.ir.symbolic.Expression | float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target: int) " + "-> None"), + R"pb(Apply a multi-controlled :math:`U_2(\phi, \lambda)` gate. + +See Also: + :meth:`u2` + +Args: + phi: The rotation angle + lambda_: The rotation angle + controls: The control qubits + target: The target qubit)pb"); + + // R + + qc.def("r", &qc::QuantumComputation::r, "theta"_a, "phi"_a, "q"_a, + R"pb(Apply an :math:`R(\theta, \phi)` gate. + +.. math:: + R(\theta, \phi) = e^{-i \frac{\theta}{2} (\cos(\phi) X + \sin(\phi) Y)} + = \begin{pmatrix} \cos(\theta / 2) & -i e^{-i \phi} \sin(\theta / 2) \\ -i e^{i \phi} \sin(\theta / 2) & \cos(\theta / 2) \end{pmatrix} + +Args: + theta: The rotation angle + phi: The rotation angle + q: The target qubit)pb"); + qc.def( + "cr", &qc::QuantumComputation::cr, "theta"_a, "phi"_a, "control"_a, + "target"_a, + nb::sig( + "def cr(self, theta: mqt.core.ir.symbolic.Expression | float, phi: " + "mqt.core.ir.symbolic.Expression " + "| float, control: mqt.core.ir.operations.Control | int, target: " + "int) -> None"), + R"pb(Apply a controlled :math:`R(\theta, \phi)` gate. + +See Also: + :meth:`r` + +Args: + theta: The rotation angle + phi: The rotation angle + control: The control qubit + target: The target qubit)pb"); + qc.def( + "mcr", &qc::QuantumComputation::mcr, "theta"_a, "phi"_a, "controls"_a, + "target"_a, + nb::sig( + "def mcr(self, theta: mqt.core.ir.symbolic.Expression | float, phi: " + "mqt.core.ir.symbolic.Expression | float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], target: " + "int) " + "-> None"), + R"pb(Apply a multi-controlled :math:`R(\theta, \phi)` gate. + +See Also: + :meth:`r` + +Args: + theta: The rotation angle + phi: The rotation angle + controls: The control qubits + target: The target qubit)pb"); + + // U + + qc.def("u", &qc::QuantumComputation::u, "theta"_a, "phi"_a, "lambda_"_a, + "q"_a, + R"pb(Apply a :math:`U(\theta, \phi, \lambda)` gate. + +.. math:: + U(\theta, \phi, \lambda) = \begin{pmatrix} \cos(\theta / 2) & -e^{i \lambda} \sin(\theta / 2) \\ e^{i \phi} \sin(\theta / 2) & e^{i (\phi + \lambda)}\cos(\theta / 2) \end{pmatrix} + +Args: + theta: The rotation angle + phi: The rotation angle + lambda_: The rotation angle + q: The target qubit)pb"); + qc.def( + "cu", &qc::QuantumComputation::cu, "theta"_a, "phi"_a, "lambda_"_a, + "control"_a, "target"_a, + nb::sig( + "def cu(self, theta: mqt.core.ir.symbolic.Expression | float, phi: " + "mqt.core.ir.symbolic.Expression | float, lambda_: " + "mqt.core.ir.symbolic.Expression | " + "float, control: mqt.core.ir.operations.Control | int, target: int) " + "-> None"), + R"pb(Apply a controlled :math:`U(\theta, \phi, \lambda)` gate. + +See Also: + :meth:`u` + +Args: + theta: The rotation angle + phi: The rotation angle + lambda_: The rotation angle + control: The control qubit + target: The target qubit)pb"); + qc.def( + "mcu", &qc::QuantumComputation::mcu, "theta"_a, "phi"_a, "lambda_"_a, + "controls"_a, "target"_a, + nb::sig( + "def mcu(self, theta: mqt.core.ir.symbolic.Expression | float, phi: " + "mqt.core.ir.symbolic.Expression | float, lambda_: " + "mqt.core.ir.symbolic.Expression | " + "float, controls: collections.abc.Set[mqt.core.ir.operations.Control " + "| " + "int], target: int) -> None"), + R"pb(Apply a multi-controlled :math:`U(\theta, \phi, \lambda)` gate. + +See Also: + :meth:`u` + +Args: + theta: The rotation angle + phi: The rotation angle + lambda_: The rotation angle + controls: The control qubits + target: The target qubit)pb"); + + // SWAP + + qc.def("swap", &qc::QuantumComputation::swap, "target1"_a, "target2"_a, + R"pb(Apply a SWAP gate. + +.. math:: + \text{SWAP} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} + +Args: + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("cswap", &qc::QuantumComputation::cswap, "control"_a, "target1"_a, + "target2"_a, + nb::sig("def cswap(self, control: mqt.core.ir.operations.Control | " + "int, target1: int, " + "target2: int) -> None"), + R"pb(Apply a controlled SWAP gate. + +See Also: + :meth:`swap` + +Args: + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcswap", &qc::QuantumComputation::mcswap, "controls"_a, "target1"_a, + "target2"_a, + nb::sig("def mcswap(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control " + "| int], target1: int, target2: int) -> None"), + R"pb(Apply a multi-controlled SWAP gate. + +See Also: + :meth:`swap` + +Args: + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // DCX + + qc.def("dcx", &qc::QuantumComputation::dcx, "target1"_a, "target2"_a, + R"pb(Apply a DCX (i.e., double CNOT) gate. + +.. math:: + DCX = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \end{pmatrix} + +Args: + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("cdcx", &qc::QuantumComputation::cdcx, "control"_a, "target1"_a, + "target2"_a, + nb::sig("def cdcx(self, control: mqt.core.ir.operations.Control | " + "int, target1: int, " + "target2: int) -> None"), + R"pb(Apply a controlled DCX gate. + +See Also: + :meth:`dcx` + +Args: + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcdcx", &qc::QuantumComputation::mcdcx, "controls"_a, "target1"_a, + "target2"_a, + nb::sig("def mcdcx(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target1: int, target2: int) -> None"), + R"pb(Apply a multi-controlled DCX gate. + +See Also: + :meth:`dcx` + +Args: + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // ECR + + qc.def("ecr", &qc::QuantumComputation::ecr, "target1"_a, "target2"_a, + R"pb(Apply a ECR (echoed cross-resonance) gate. + +.. math:: + ECR = \frac{1}{\sqrt{2}} \begin{pmatrix} 0 & 0 & 1 & i \\ 0 & 0 & i & 1 \\ 1 & -i & 0 & 0 \\ -i & 1 & 0 & 0 \end{pmatrix} + +Args: + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("cecr", &qc::QuantumComputation::cecr, "control"_a, "target1"_a, + "target2"_a, + nb::sig("def cecr(self, control: mqt.core.ir.operations.Control | " + "int, target1: int, " + "target2: int) -> None"), + R"pb(Apply a controlled ECR gate. + +See Also: + :meth:`ecr` + +Args: + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcecr", &qc::QuantumComputation::mcecr, "controls"_a, "target1"_a, + "target2"_a, + nb::sig("def mcecr(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | " + "int], target1: int, target2: int) -> None"), + R"pb(Apply a multi-controlled ECR gate. + +See Also: + :meth:`ecr` + +Args: + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // iSWAP + + qc.def("iswap", &qc::QuantumComputation::iswap, "target1"_a, "target2"_a, + R"pb(Apply a :math:`i\text{SWAP}` gate. + +.. math:: + i\text{SWAP} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & i & 0 \\ 0 & i & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} + +Args: + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("ciswap", &qc::QuantumComputation::ciswap, "control"_a, "target1"_a, + "target2"_a, + nb::sig("def ciswap(self, control: mqt.core.ir.operations.Control | " + "int, target1: int, " + "target2: int) -> None"), + R"pb(Apply a controlled :math:`i\text{SWAP}` gate. + +See Also: + :meth:`iswap` + +Args: + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mciswap", &qc::QuantumComputation::mciswap, "controls"_a, "target1"_a, + "target2"_a, + nb::sig("def mciswap(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control " + "| int], target1: int, target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`i\text{SWAP}` gate. + +See Also: + :meth:`iswap` + +Args: + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // iSWAPdg + + qc.def("iswapdg", &qc::QuantumComputation::iswapdg, "target1"_a, "target2"_a, + R"pb(Apply a :math:`i\text{SWAP}^\dagger` gate. + +.. math:: + i\text{SWAP}^\dagger = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & -i & 0 \\ 0 & -i & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} + +Args: + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("ciswapdg", &qc::QuantumComputation::ciswapdg, "control"_a, + "target1"_a, "target2"_a, + nb::sig("def ciswapdg(self, control: mqt.core.ir.operations.Control | " + "int, target1: " + "int, target2: int) -> None"), + R"pb(Apply a controlled :math:`i\text{SWAP}^\dagger` gate. + +See Also: + :meth:`iswapdg` + +Args: + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mciswapdg", &qc::QuantumComputation::mciswapdg, "controls"_a, + "target1"_a, "target2"_a, + nb::sig("def mciswapdg(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control " + "| int], target1: int, target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`i\text{SWAP}^\dagger` gate. + +See Also: + :meth:`iswapdg` + +Args: + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // Peres + + qc.def("peres", &qc::QuantumComputation::peres, "target1"_a, "target2"_a, + R"pb(Apply a Peres gate. + +.. math:: + \text{Peres} = \begin{pmatrix} 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \end{pmatrix} + +Args: + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("cperes", &qc::QuantumComputation::cperes, "control"_a, "target1"_a, + "target2"_a, + nb::sig("def cperes(self, control: mqt.core.ir.operations.Control | " + "int, target1: int, " + "target2: int) -> None"), + R"pb(Apply a controlled Peres gate. + +See Also: + :meth:`peres` + +Args: + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcperes", &qc::QuantumComputation::mcperes, "controls"_a, "target1"_a, + "target2"_a, + nb::sig("def mcperes(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control " + "| int], target1: int, target2: int) -> None"), + R"pb(Apply a multi-controlled Peres gate. + +See Also: + :meth:`peres` + +Args: + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // Peresdg + + qc.def("peresdg", &qc::QuantumComputation::peresdg, "target1"_a, "target2"_a, + R"pb(Apply a :math:`\text{Peres}^\dagger` gate. + +.. math:: + \text{Peres}^\dagger = \begin{pmatrix} 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \end{pmatrix} +Args: + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("cperesdg", &qc::QuantumComputation::cperesdg, "control"_a, + "target1"_a, "target2"_a, + nb::sig("def cperesdg(self, control: mqt.core.ir.operations.Control | " + "int, target1: " + "int, target2: int) -> None"), + R"pb(Apply a controlled :math:`\text{Peres}^\dagger` gate. + +See Also: + :meth:`peresdg` + +Args: + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcperesdg", &qc::QuantumComputation::mcperesdg, "controls"_a, + "target1"_a, "target2"_a, + nb::sig("def mcperesdg(self, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control " + "| int], target1: int, target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`\text{Peres}^\dagger` gate. + +See Also: + :meth:`peresdg` + +Args: + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // RXX + + qc.def("rxx", &qc::QuantumComputation::rxx, "theta"_a, "target1"_a, + "target2"_a, + R"pb(Apply an :math:`R_{xx}(\theta)` gate. + +.. math:: + R_{xx}(\theta) = e^{-i \theta XX / 2} = \cos(\theta / 2) I \otimes I - i \sin(\theta / 2) X \otimes X + = \begin{pmatrix} \cos(\theta / 2) & 0 & 0 & -i \sin(\theta / 2) \\ + 0 & \cos(\theta / 2) & -i \sin(\theta / 2) & 0 \\ + 0 & -i \sin(\theta / 2) & \cos(\theta / 2) & 0 \\ + -i \sin(\theta / 2) & 0 & 0 & \cos(\theta / 2) \end{pmatrix} + +Args: + theta: The rotation angle + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("crxx", &qc::QuantumComputation::crxx, "theta"_a, "control"_a, + "target1"_a, "target2"_a, + nb::sig("def crxx(self, theta: mqt.core.ir.symbolic.Expression | " + "float, control: " + "mqt.core.ir.operations.Control | int, target1: int, target2: " + "int) -> None"), + R"pb(Apply a controlled :math:`R_{xx}(\theta)` gate. + +See Also: + :meth:`rxx` + +Args: + theta: The rotation angle + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcrxx", &qc::QuantumComputation::mcrxx, "theta"_a, "controls"_a, + "target1"_a, "target2"_a, + nb::sig("def mcrxx(self, theta: mqt.core.ir.symbolic.Expression | " + "float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target1: int, " + "target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`R_{xx}(\theta)` gate. + +See Also: + :meth:`rxx` + +Args: + theta: The rotation angle + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // RYY + + qc.def("ryy", &qc::QuantumComputation::ryy, "theta"_a, "target1"_a, + "target2"_a, + R"pb(Apply an :math:`R_{yy}(\theta)` gate. + +.. math:: + R_{yy}(\theta) = e^{-i \theta YY / 2} = \cos(\theta / 2) I \otimes I - i \sin(\theta / 2) Y \otimes Y + = \begin{pmatrix} \cos(\theta / 2) & 0 & 0 & i \sin(\theta / 2) \\ + 0 & \cos(\theta / 2) & -i \sin(\theta / 2) & 0 \\ + 0 & -i \sin(\theta / 2) & \cos(\theta / 2) & 0 \\ + i \sin(\theta / 2) & 0 & 0 & \cos(\theta / 2) \end{pmatrix} + +Args: + theta: The rotation angle + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("cryy", &qc::QuantumComputation::cryy, "theta"_a, "control"_a, + "target1"_a, "target2"_a, + nb::sig("def cryy(self, theta: mqt.core.ir.symbolic.Expression | " + "float, control: " + "mqt.core.ir.operations.Control | int, target1: int, target2: " + "int) -> None"), + R"pb(Apply a controlled :math:`R_{yy}(\theta)` gate. + +See Also: + :meth:`ryy` + +Args: + theta: The rotation angle + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcryy", &qc::QuantumComputation::mcryy, "theta"_a, "controls"_a, + "target1"_a, "target2"_a, + nb::sig("def mcryy(self, theta: mqt.core.ir.symbolic.Expression | " + "float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target1: int, " + "target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`R_{yy}(\theta)` gate. + +See Also: + :meth:`ryy` + +Args: + theta: The rotation angle + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // RZX + + qc.def("rzx", &qc::QuantumComputation::rzx, "theta"_a, "target1"_a, + "target2"_a, + R"pb(Apply an :math:`R_{zx}(\theta)` gate. + +.. math:: + R_{zx}(\theta) = e^{-i \theta ZX / 2} = \cos(\theta / 2) I \otimes I - i \sin(\theta / 2) Z \otimes X + = \begin{pmatrix} \cos(\theta/2) & -i \sin(\theta/2) & 0 & 0 \\ + -i \sin(\theta/2) & \cos(\theta/2) & 0 & 0 \\ + 0 & 0 & \cos(\theta/2) & i \sin(\theta/2) \\ + 0 & 0 & i \sin(\theta/2) & \cos(\theta/2) \end{pmatrix} + +Args: + theta: The rotation angle + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("crzx", &qc::QuantumComputation::crzx, "theta"_a, "control"_a, + "target1"_a, "target2"_a, + nb::sig("def crzx(self, theta: mqt.core.ir.symbolic.Expression | " + "float, control: " + "mqt.core.ir.operations.Control | int, target1: int, target2: " + "int) -> None"), + R"pb(Apply a controlled :math:`R_{zx}(\theta)` gate. + +See Also: + :meth:`rzx` + +Args: + theta: The rotation angle + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcrzx", &qc::QuantumComputation::mcrzx, "theta"_a, "controls"_a, + "target1"_a, "target2"_a, + nb::sig("def mcrzx(self, theta: mqt.core.ir.symbolic.Expression | " + "float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target1: int, " + "target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`R_{zx}(\theta)` gate. + +See Also: + :meth:`rzx` + +Args: + theta: The rotation angle + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // RZZ + + qc.def("rzz", &qc::QuantumComputation::rzz, "theta"_a, "target1"_a, + "target2"_a, + R"pb(Apply an :math:`R_{zz}(\theta)` gate. + +.. math:: + R_{zz}(\theta) = e^{-i \theta ZZ / 2} + = \begin{pmatrix} e^{-i \theta / 2} & 0 & 0 & 0 \\ + 0 & e^{i \theta / 2} & 0 & 0 \\ + 0 & 0 & e^{i \theta / 2} & 0 \\ + 0 & 0 & 0 & e^{-i \theta / 2} \end{pmatrix} + +Args: + theta: The rotation angle + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("crzz", &qc::QuantumComputation::crzz, "theta"_a, "control"_a, + "target1"_a, "target2"_a, + nb::sig("def crzz(self, theta: mqt.core.ir.symbolic.Expression | " + "float, control: " + "mqt.core.ir.operations.Control | int, target1: int, target2: " + "int) -> None"), + R"pb(Apply a controlled :math:`R_{zz}(\theta)` gate. + +See Also: + :meth:`rzz` + +Args: + theta: The rotation angle + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcrzz", &qc::QuantumComputation::mcrzz, "theta"_a, "controls"_a, + "target1"_a, "target2"_a, + nb::sig("def mcrzz(self, theta: mqt.core.ir.symbolic.Expression | " + "float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target1: int, " + "target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`R_{zz}(\theta)` gate. + +See Also: + :meth:`rzz` + +Args: + theta: The rotation angle + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // XXMinusYY + + qc.def("xx_minus_yy", &qc::QuantumComputation::xx_minus_yy, "theta"_a, + "beta"_a, "target1"_a, "target2"_a, + R"pb(Apply an :math:`R_{XX - YY}(\theta, \beta)` gate. + +.. math:: + R_{XX - YY}(\theta, \beta) = R_{z_2}(\beta) \cdot e^{-i \frac{\theta}{2} \frac{XX - YY}{2}} \cdot R_{z_2}(-\beta) + = \begin{pmatrix} \cos(\theta / 2) & 0 & 0 & -i \sin(\theta / 2) e^{-i \beta} \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + -i \sin(\theta / 2) e^{i \beta} & 0 & 0 & \cos(\theta / 2) \end{pmatrix} + +Args: + theta: The rotation angle + beta: The rotation angle + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("cxx_minus_yy", &qc::QuantumComputation::cxx_minus_yy, "theta"_a, + "beta"_a, "control"_a, "target1"_a, "target2"_a, + nb::sig("def cxx_minus_yy(self, theta: " + "mqt.core.ir.symbolic.Expression | float, beta: " + "mqt.core.ir.symbolic.Expression | float, control: " + "mqt.core.ir.operations.Control | " + "int, target1: int, target2: int) -> None"), + R"pb(Apply a controlled :math:`R_{XX - YY}(\theta, \beta)` gate. + +See Also: + :meth:`xx_minus_yy` + +Args: + theta: The rotation angle + beta: The rotation angle + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcxx_minus_yy", &qc::QuantumComputation::mcxx_minus_yy, "theta"_a, + "beta"_a, "controls"_a, "target1"_a, "target2"_a, + nb::sig("def mcxx_minus_yy(self, theta: " + "mqt.core.ir.symbolic.Expression | float, beta: " + "mqt.core.ir.symbolic.Expression | float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target1: int, " + "target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`R_{XX - YY}(\theta, \beta)` gate. + +See Also: + :meth:`xx_minus_yy` + +Args: + theta: The rotation angle + beta: The rotation angle + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + // XXPlusYY + + qc.def("xx_plus_yy", &qc::QuantumComputation::xx_plus_yy, "theta"_a, "beta"_a, + "target1"_a, "target2"_a, + R"pb(Apply an :math:`R_{XX + YY}(\theta, \beta)` gate. + +.. math:: + R_{XX + YY}(\theta, \beta) = R_{z_1}(\beta) \cdot e^{-i \frac{\theta}{2} \frac{XX + YY}{2}} \cdot R_{z_1}(-\beta) + = \begin{pmatrix} 1 & 0 & 0 & 0 \\ + 0 & \cos(\theta / 2) & -i \sin(\theta / 2) e^{-i \beta} & 0 \\ + 0 & -i \sin(\theta / 2) e^{i \beta} & \cos(\theta / 2) & 0 \\ + 0 & 0 & 0 & 1 \end{pmatrix} + +Args: + theta: The rotation angle + beta: The rotation angle + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("cxx_plus_yy", &qc::QuantumComputation::cxx_plus_yy, "theta"_a, + "beta"_a, "control"_a, "target1"_a, "target2"_a, + nb::sig("def cxx_plus_yy(self, theta: mqt.core.ir.symbolic.Expression " + "| float, beta: " + "mqt.core.ir.symbolic.Expression | float, control: " + "mqt.core.ir.operations.Control | " + "int, target1: int, target2: int) -> None"), + R"pb(Apply a controlled :math:`R_{XX + YY}(\theta, \beta)` gate. + +See Also: + :meth:`xx_plus_yy` + +Args: + theta: The rotation angle + beta: The rotation angle + control: The control qubit + target1: The first target qubit + target2: The second target qubit)pb"); + qc.def("mcxx_plus_yy", &qc::QuantumComputation::mcxx_plus_yy, "theta"_a, + "beta"_a, "controls"_a, "target1"_a, "target2"_a, + nb::sig("def mcxx_plus_yy(self, theta: " + "mqt.core.ir.symbolic.Expression | float, beta: " + "mqt.core.ir.symbolic.Expression | float, controls: " + "collections.abc.Set[mqt.core.ir.operations.Control | int], " + "target1: int, " + "target2: int) -> None"), + R"pb(Apply a multi-controlled :math:`R_{XX + YY}(\theta, \beta)` gate. + +See Also: + :meth:`xx_plus_yy` + +Args: + theta: The rotation angle + beta: The rotation angle + controls: The control qubits + target1: The first target qubit + target2: The second target qubit)pb"); + + qc.def("gphase", &qc::QuantumComputation::gphase, "phase"_a, + R"pb(Apply a global phase gate. + +.. math:: + GPhase(\theta) = (e^{i \theta}) + +Args: + phase: The rotation angle)pb"); qc.def("measure", - py::overload_cast( + nb::overload_cast( &qc::QuantumComputation::measure), - "qubit"_a, "cbit"_a); + "qubit"_a, "cbit"_a, + R"pb(Measure a qubit and store the result in a classical bit. + +Args: + qubit: The qubit to measure + cbit: The classical bit to store the result)pb"); + qc.def("measure", - py::overload_cast&, + nb::overload_cast&, const std::vector&>( &qc::QuantumComputation::measure), - "qubits"_a, "cbits"_a); - qc.def("measure_all", &qc::QuantumComputation::measureAll, py::kw_only(), - "add_bits"_a = true); + "qubits"_a, "cbits"_a, + R"pb(Measure multiple qubits and store the results in classical bits. + +This method is equivalent to calling :meth:`measure` multiple times. + +Args: + qubits: The qubits to measure + cbits: The classical bits to store the results)pb"); + qc.def("measure_all", &qc::QuantumComputation::measureAll, nb::kw_only(), + "add_bits"_a = true, + R"pb(Measure all qubits and store the results in classical bits. + +Details: + If `add_bits` is `True`, a new classical register (named "`meas`") with the same size as the number of qubits will be added to the circuit and the results will be stored in it. + If `add_bits` is `False`, the classical register must already exist and have a sufficient number of bits to store the results. + +Args: + add_bits: Whether to explicitly add a classical register)pb"); + + qc.def("reset", nb::overload_cast(&qc::QuantumComputation::reset), + "q"_a, R"pb(Add a reset operation to the circuit. + +Args: + q: The qubit to reset)pb"); - qc.def("reset", py::overload_cast(&qc::QuantumComputation::reset), - "q"_a); qc.def("reset", - py::overload_cast&>( + nb::overload_cast&>( &qc::QuantumComputation::reset), - "qubits"_a); + "qubits"_a, R"pb(Add a reset operation to the circuit. + +Args: + qubits: The qubits to reset)pb"); + + qc.def("barrier", nb::overload_cast<>(&qc::QuantumComputation::barrier), + "Add a barrier to the circuit."); + + qc.def("barrier", + nb::overload_cast(&qc::QuantumComputation::barrier), "q"_a, + R"pb(Add a barrier to the circuit. + +Args: + q: The qubit to add the barrier to)pb"); - qc.def("barrier", py::overload_cast<>(&qc::QuantumComputation::barrier)); qc.def("barrier", - py::overload_cast(&qc::QuantumComputation::barrier), "q"_a); - qc.def("barrier", py::overload_cast&>( - &qc::QuantumComputation::barrier)); + nb::overload_cast&>( + &qc::QuantumComputation::barrier), + "qubits"_a, R"pb(Add a barrier to the circuit. + +Args: + qubits: The qubits to add the barrier to)pb"); qc.def( "if_else", @@ -445,7 +2058,16 @@ void registerQuantumComputation(py::module& m) { expectedValue, kind); }, "then_operation"_a, "else_operation"_a, "control_register"_a, - "expected_value"_a = 1U, "comparison_kind"_a = qc::ComparisonKind::Eq); + "expected_value"_a = 1U, "comparison_kind"_a = qc::ComparisonKind::Eq, + R"pb(Add an if-else operation to the circuit. + +Args: + then_operation: The operation to apply if the condition is met + else_operation: The operation to apply if the condition is not met + control_register: The classical register to check against + expected_value: The expected value of the control register + comparison_kind: The kind of comparison to perform)pb"); + qc.def( "if_else", [](qc::QuantumComputation& self, qc::Operation* thenOp, @@ -460,61 +2082,164 @@ void registerQuantumComputation(py::module& m) { expectedValue, kind); }, "then_operation"_a, "else_operation"_a, "control_bit"_a, - "expected_value"_a = 1U, "comparison_kind"_a = qc::ComparisonKind::Eq); + "expected_value"_a = 1U, "comparison_kind"_a = qc::ComparisonKind::Eq, + R"pb(Add an if-else operation to the circuit. + +Args: + then_operation: The operation to apply if the condition is met + else_operation: The operation to apply if the condition is not met + control_bit: The index of the classical bit to check against + expected_value: The expected value of the control bit + comparison_kind: The kind of comparison to perform)pb"); qc.def( "if_", - py::overload_cast&>( &qc::QuantumComputation::if_), "op_type"_a, "target"_a, "control_register"_a, "expected_value"_a = 1U, "comparison_kind"_a = qc::ComparisonKind::Eq, - "params"_a = std::vector{}); + "params"_a.sig("...") = std::vector{}, + R"pb(Add an if operation to the circuit. + +Args: + op_type: The operation to apply + target: The target qubit + control_register: The classical register to check against + expected_value: The expected value of the control register + comparison_kind: The kind of comparison to perform + params: The parameters of the operation)pb"); + qc.def( "if_", - py::overload_cast&>( &qc::QuantumComputation::if_), "op_type"_a, "target"_a, "control"_a, "control_register"_a, "expected_value"_a = 1U, "comparison_kind"_a = qc::ComparisonKind::Eq, - "params"_a = std::vector{}); + "params"_a = std::vector{}, + nb::sig( + "def if_(self, op_type: mqt.core.ir.operations.OpType, target: " + "int, control: mqt.core.ir.operations.Control | int, " + "control_register: mqt.core.ir.registers.ClassicalRegister, " + "expected_value: int = 1, comparison_kind: " + "mqt.core.ir.operations.ComparisonKind = ..., params: " + "collections.abc.Sequence[mqt.core.ir.symbolic.Expression | float] " + "= ...) -> None"), + R"pb(Add an if operation to the circuit. + +Args: + op_type: The operation to apply + target: The target qubit + control: The control qubit + control_register: The classical register to check against + expected_value: The expected value of the control register + comparison_kind: The kind of comparison to perform + params: The parameters of the operation.)pb"); + qc.def( "if_", - py::overload_cast&>( &qc::QuantumComputation::if_), "op_type"_a, "target"_a, "controls"_a, "control_register"_a, "expected_value"_a = 1U, "comparison_kind"_a = qc::ComparisonKind::Eq, - "params"_a = std::vector{}); + "params"_a = std::vector{}, + nb::sig( + "def if_(self, op_type: mqt.core.ir.operations.OpType, target: " + "int, controls: collections.abc.Set[mqt.core.ir.operations.Control | " + "int], " + "control_register: mqt.core.ir.registers.ClassicalRegister, " + "expected_value: int = 1, comparison_kind: " + "mqt.core.ir.operations.ComparisonKind = ..., params: " + "collections.abc.Sequence[mqt.core.ir.symbolic.Expression | float] " + "= ...) -> None"), + R"pb(Add an if operation to the circuit. + +Args: + op_type: The operation to apply + target: The target qubit + controls: The control qubits + control_register: The classical register to check against + expected_value: The expected value of the control register + comparison_kind: The kind of comparison to perform + params: The parameters of the operation.)pb"); + qc.def("if_", - py::overload_cast&>( &qc::QuantumComputation::if_), "op_type"_a, "target"_a, "control_bit"_a, "expected_value"_a = true, "comparison_kind"_a = qc::ComparisonKind::Eq, - "params"_a = std::vector{}); - qc.def("if_", - py::overload_cast&>( - &qc::QuantumComputation::if_), - "op_type"_a, "target"_a, "control"_a, "control_bit"_a, - "expected_value"_a = true, - "comparison_kind"_a = qc::ComparisonKind::Eq, - "params"_a = std::vector{}); + "params"_a.sig("...") = std::vector{}, + R"pb(Add an if operation to the circuit. + +Args: + op_type: The operation to apply + target: The target qubit + control_bit: The index of the classical bit to check against + expected_value: The expected value of the control bit + comparison_kind: The kind of comparison to perform + params: The parameters of the operation.)pb"); + qc.def( "if_", - py::overload_cast&>( + &qc::QuantumComputation::if_), + "op_type"_a, "target"_a, "control"_a, "control_bit"_a, + "expected_value"_a = true, "comparison_kind"_a = qc::ComparisonKind::Eq, + "params"_a = std::vector{}, + nb::sig("def if_(self, op_type: mqt.core.ir.operations.OpType, target: " + "int, control: mqt.core.ir.operations.Control | int, " + "control_bit: int, expected_value: bool = True, " + "comparison_kind: " + "mqt.core.ir.operations.ComparisonKind = ..., params: " + "collections.abc.Sequence[mqt.core.ir.symbolic.Expression | " + "float] = ...) -> None"), + R"pb(Add an if operation to the circuit. + +Args: + op_type: The operation to apply + target: The target qubit + control: The control qubit + control_bit: The index of the classical bit to check against + expected_value: The expected value of the control bit + comparison_kind: The kind of comparison to perform + params: The parameters of the operation.)pb"); + + qc.def( + "if_", + nb::overload_cast&>( &qc::QuantumComputation::if_), "op_type"_a, "target"_a, "controls"_a, "control_bit"_a, "expected_value"_a = true, "comparison_kind"_a = qc::ComparisonKind::Eq, - "params"_a = std::vector{}); + "params"_a = std::vector{}, + nb::sig( + "def if_(self, op_type: mqt.core.ir.operations.OpType, target: " + "int, controls: collections.abc.Set[mqt.core.ir.operations.Control " + "| int], control_bit: int, expected_value: bool = True, " + "comparison_kind: " + "mqt.core.ir.operations.ComparisonKind = ..., params: " + "collections.abc.Sequence[mqt.core.ir.symbolic.Expression | " + "float] = ...) -> None"), + R"pb(Add an if operation to the circuit. + +Args: + op_type: The operation to apply + target: The target qubit + controls: The control qubits + control_bit: The index of the classical bit to check against + expected_value: The expected value of the control bit + comparison_kind: The kind of comparison to perform + params: The parameters of the operation.)pb"); } } // namespace mqt diff --git a/bindings/ir/register_registers.cpp b/bindings/ir/register_registers.cpp index 991327d7f7..6920a28e0c 100644 --- a/bindings/ir/register_registers.cpp +++ b/bindings/ir/register_registers.cpp @@ -11,81 +11,178 @@ #include "ir/Definitions.hpp" #include "ir/Register.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -// clang-format on - #include +#include +#include +#include +#include +#include // NOLINT(misc-include-cleaner) #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerRegisters(py::module& m) { - py::class_(m, "QuantumRegister") - .def(py::init(), +void registerRegisters(const nb::module_& m) { + nb::class_( + m, "QuantumRegister", R"pb(A class to represent a collection of qubits. + +Args: + start: The starting index of the quantum register. + size: The number of qubits in the quantum register. + name: The name of the quantum register. A name will be generated if not provided.)pb") + + .def(nb::init(), "start"_a, "size"_a, "name"_a = "") - .def_property_readonly( - "name", [](const qc::QuantumRegister& reg) { return reg.getName(); }) - .def_property( + + .def_prop_ro( + "name", [](const qc::QuantumRegister& reg) { return reg.getName(); }, + "The name of the quantum register.") + + .def_prop_rw( "start", [](const qc::QuantumRegister& reg) { return reg.getStartIndex(); }, - [](qc::QuantumRegister& reg, const qc::Qubit start) { - reg.getStartIndex() = start; - }) - .def_property( + [](qc::QuantumRegister& reg, const nb::int_& start) { + const auto startInt = static_cast(start); + if (startInt < 0) { + throw nb::value_error("Start index cannot be negative"); + } + const auto startUint = static_cast(startInt); + if (startUint > std::numeric_limits::max()) { + throw nb::value_error("Start index exceeds maximum value"); + } + reg.getStartIndex() = static_cast(startUint); + }, + "The index of the first qubit in the quantum register.") + .def_prop_rw( "size", [](const qc::QuantumRegister& reg) { return reg.getSize(); }, - [](qc::QuantumRegister& reg, const std::size_t size) { - reg.getSize() = size; - }) - .def_property_readonly( + [](qc::QuantumRegister& reg, const nb::int_& size) { + const auto sizeInt = static_cast(size); + if (sizeInt < 0) { + throw nb::value_error("Size cannot be negative"); + } + const auto sizeUint = static_cast(sizeInt); + if (sizeUint > std::numeric_limits::max()) { + throw nb::value_error("Size exceeds maximum value"); + } + reg.getSize() = static_cast(sizeUint); + }, + "The number of qubits in the quantum register.") + .def_prop_ro( "end", - [](const qc::QuantumRegister& reg) { return reg.getEndIndex(); }) - .def(py::self == py::self) // NOLINT(misc-redundant-expression) - .def(py::self != py::self) // NOLINT(misc-redundant-expression) - .def(hash(py::self)) - .def("__getitem__", &qc::QuantumRegister::getGlobalIndex, "key"_a) - .def("__contains__", &qc::QuantumRegister::contains) + [](const qc::QuantumRegister& reg) { return reg.getEndIndex(); }, + "Index of the last qubit in the quantum register.") + + .def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")) + .def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")) + .def(nb::hash(nb::self)) + + .def( + "__getitem__", + [](const qc::QuantumRegister& reg, nb::ssize_t idx) { + const auto n = static_cast(reg.getSize()); + if (idx < 0) { + idx += n; + } + if (idx < 0 || idx >= n) { + throw nb::index_error(); + } + return reg.getGlobalIndex(static_cast(idx)); + }, + "key"_a, "Get the qubit at the specified index.") + + .def("__contains__", &qc::QuantumRegister::contains, "item"_a, + "Check if the quantum register contains a qubit.") + .def("__repr__", [](const qc::QuantumRegister& reg) { return "QuantumRegister(name=" + reg.getName() + ", start=" + std::to_string(reg.getStartIndex()) + ", size=" + std::to_string(reg.getSize()) + ")"; }); - py::class_(m, "ClassicalRegister") - .def(py::init(), + nb::class_( + m, "ClassicalRegister", + R"pb(A class to represent a collection of classical bits. + +Args: + start: The starting index of the classical register. + size: The number of bits in the classical register. + name: The name of the classical register. A name will be generated if not provided.)pb") + + .def(nb::init(), "start"_a, "size"_a, "name"_a = "") - .def_property_readonly( + + .def_prop_ro( "name", - [](const qc::ClassicalRegister& reg) { return reg.getName(); }) - .def_property( + [](const qc::ClassicalRegister& reg) { return reg.getName(); }, + "The name of the classical register.") + + .def_prop_rw( "start", [](const qc::ClassicalRegister& reg) { return reg.getStartIndex(); }, - [](qc::ClassicalRegister& reg, const qc::Bit start) { - reg.getStartIndex() = start; - }) - .def_property( + [](qc::ClassicalRegister& reg, const nb::int_& start) { + const auto startInt = static_cast(start); + if (startInt < 0) { + throw nb::value_error("Start index cannot be negative"); + } + const auto startUint = static_cast(startInt); + if (startUint > std::numeric_limits::max()) { + throw nb::value_error("Start index exceeds maximum value"); + } + reg.getStartIndex() = static_cast(startUint); + }, + "The index of the first bit in the classical register.") + + .def_prop_rw( "size", [](const qc::ClassicalRegister& reg) { return reg.getSize(); }, - [](qc::ClassicalRegister& reg, const std::size_t size) { - reg.getSize() = size; - }) - .def_property_readonly( + [](qc::ClassicalRegister& reg, const nb::int_& size) { + const auto sizeInt = static_cast(size); + if (sizeInt < 0) { + throw nb::value_error("Size cannot be negative"); + } + const auto sizeUint = static_cast(sizeInt); + if (sizeUint > std::numeric_limits::max()) { + throw nb::value_error("Size exceeds maximum value"); + } + reg.getSize() = static_cast(sizeUint); + }, + "The number of bits in the classical register.") + + .def_prop_ro( "end", - [](const qc::ClassicalRegister& reg) { return reg.getEndIndex(); }) - .def(py::self == py::self) // NOLINT(misc-redundant-expression) - .def(py::self != py::self) // NOLINT(misc-redundant-expression) - .def(hash(py::self)) - .def("__getitem__", &qc::ClassicalRegister::getGlobalIndex, "key"_a) - .def("__contains__", &qc::ClassicalRegister::contains) + [](const qc::ClassicalRegister& reg) { return reg.getEndIndex(); }, + "Index of the last bit in the classical register.") + + // NOLINTNEXTLINE(misc-redundant-expression) + .def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")) + // NOLINTNEXTLINE(misc-redundant-expression) + .def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")) + .def(nb::hash(nb::self)) + + .def( + "__getitem__", + [](const qc::ClassicalRegister& reg, nb::ssize_t idx) { + const auto n = static_cast(reg.getSize()); + if (idx < 0) { + idx += n; + } + if (idx < 0 || idx >= n) { + throw nb::index_error(); + } + return reg.getGlobalIndex(static_cast(idx)); + }, + "key"_a, "Get the bit at the specified index.") + + .def("__contains__", &qc::ClassicalRegister::contains, "item"_a, + "Check if the classical register contains a bit.") + .def("__repr__", [](const qc::ClassicalRegister& reg) { return "ClassicalRegister(name=" + reg.getName() + ", start=" + std::to_string(reg.getStartIndex()) + diff --git a/bindings/ir/register_symbolic.cpp b/bindings/ir/register_symbolic.cpp index c9cd9f4d8e..cd8eefe5f5 100644 --- a/bindings/ir/register_symbolic.cpp +++ b/bindings/ir/register_symbolic.cpp @@ -8,24 +8,19 @@ * Licensed under the MIT License */ -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) -// clang-format on +#include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; // forward declarations -void registerVariable(py::module& m); -void registerTerm(py::module& m); -void registerExpression(py::module& m); +void registerVariable(const nb::module_& m); +void registerTerm(const nb::module_& m); +void registerExpression(const nb::module_& m); // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerSymbolic(pybind11::module& m) { +void registerSymbolic(const nb::module_& m) { registerVariable(m); registerTerm(m); registerExpression(m); diff --git a/bindings/ir/symbolic/register_expression.cpp b/bindings/ir/symbolic/register_expression.cpp index b9e546f310..a45a0a5367 100644 --- a/bindings/ir/symbolic/register_expression.cpp +++ b/bindings/ir/symbolic/register_expression.cpp @@ -10,93 +10,145 @@ #include "ir/operations/Expression.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -#include -// clang-format on - #include +#include +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerExpression(py::module& m) { - py::class_>(m, "Expression") - .def(py::init([](const std::vector>& terms, - double constant) { - return sym::Expression(terms, constant); - }), - "terms"_a, "constant"_a = 0.0) - .def(py::init([](const sym::Term& term, double constant) { - return sym::Expression( - std::vector>{term}, constant); - }), - "term"_a, "constant"_a = 0.0) - .def(py::init(), "constant"_a = 0.0) - .def_property("constant", &sym::Expression::getConst, - &sym::Expression::setConst) +void registerExpression(const nb::module_& m) { + nb::class_>( + m, "Expression", + R"pb(A symbolic expression which consists of a sum of terms and a constant. + +The expression is of the form :math:`constant + term_1 + term_2 + \dots + term_n`. +Alternatively, an expression can be created with a single term and a constant or just a constant. + +Args: + terms: The list of terms. + constant: The constant.)pb") + + .def(nb::init(), "constant"_a = 0.0) + .def(nb::init>&, double>(), "terms"_a, + "constant"_a = 0.0) + .def( + "__init__", + [](sym::Expression* self, + const sym::Term& term, double constant) { + new (self) sym::Expression( + std::vector>{term}, constant); + }, + "term"_a, "constant"_a = 0.0) + + .def_prop_rw("constant", &sym::Expression::getConst, + &sym::Expression::setConst, + "The constant of the expression.") .def( "__iter__", [](const sym::Expression& expr) { - return py::make_iterator(expr.begin(), expr.end()); + return make_iterator(nb::type>(), + "iterator", expr.begin(), expr.end()); }, - py::keep_alive<0, 1>()) - .def("__getitem__", - [](const sym::Expression& expr, - const std::size_t idx) { - if (idx >= expr.numTerms()) { - throw py::index_error(); - } - return expr.getTerms()[idx]; - }) - .def("is_zero", &sym::Expression::isZero) - .def("is_constant", &sym::Expression::isConstant) - .def("num_terms", &sym::Expression::numTerms) + nb::keep_alive<0, 1>()) + + .def( + "__getitem__", + [](const sym::Expression& expr, nb::ssize_t idx) { + const auto n = static_cast(expr.numTerms()); + if (idx < 0) { + idx += n; + } + if (idx < 0 || idx >= n) { + throw nb::index_error(); + } + // NOLINTNEXTLINE(*-pro-bounds-avoid-unchecked-container-access) + return expr.getTerms()[static_cast(idx)]; + }, + "index"_a) + + .def("is_zero", &sym::Expression::isZero, + "Check if the expression is zero.") + + .def("is_constant", &sym::Expression::isConstant, + "Check if the expression is a constant.") + + .def("num_terms", &sym::Expression::numTerms, + "The number of terms in the expression.") + .def("__len__", &sym::Expression::numTerms) - .def_property_readonly("terms", - &sym::Expression::getTerms) - .def_property_readonly("variables", - &sym::Expression::getVariables) + + .def_prop_ro("terms", &sym::Expression::getTerms, + "The terms of the expression.") + + .def_prop_ro("variables", &sym::Expression::getVariables, + "The variables in the expression.") + .def("evaluate", &sym::Expression::evaluate, - "assignment"_a) + "assignment"_a, + R"pb(Evaluate the expression with a given variable assignment. + +Args: + assignment: The variable assignment. + +Returns: + The evaluated value of the expression.)pb") + // addition operators - .def(py::self + py::self) - .def(py::self + double()) - .def("__add__", [](const sym::Expression& lhs, - const sym::Term& rhs) { return lhs + rhs; }) - .def("__radd__", [](const sym::Expression& rhs, - const sym::Term& lhs) { return lhs + rhs; }) - .def("__radd__", [](const sym::Expression& rhs, - const double lhs) { return rhs + lhs; }) + .def(nb::self + nb::self, nb::is_operator()) + .def(nb::self + double(), nb::is_operator()) + .def( + "__add__", + [](const sym::Expression& lhs, + const sym::Term& rhs) { return lhs + rhs; }, + nb::is_operator()) + .def( + "__radd__", + [](const sym::Expression& rhs, + const sym::Term& lhs) { return lhs + rhs; }, + nb::is_operator()) + .def( + "__radd__", + [](const sym::Expression& rhs, const double lhs) { + return rhs + lhs; + }, + nb::is_operator()) // subtraction operators - .def(py::self - py::self) // NOLINT(misc-redundant-expression) - .def(py::self - double()) - .def(double() - py::self) - .def("__sub__", [](const sym::Expression& lhs, - const sym::Term& rhs) { return lhs - rhs; }) - .def("__rsub__", [](const sym::Expression& rhs, - const sym::Term& lhs) { return lhs - rhs; }) + // NOLINTNEXTLINE(misc-redundant-expression) + .def(nb::self - nb::self, nb::is_operator()) + .def(nb::self - double(), nb::is_operator()) + .def(double() - nb::self, nb::is_operator()) + .def( + "__sub__", + [](const sym::Expression& lhs, + const sym::Term& rhs) { return lhs - rhs; }, + nb::is_operator()) + .def( + "__rsub__", + [](const sym::Expression& rhs, + const sym::Term& lhs) { return lhs - rhs; }, + nb::is_operator()) // multiplication operators - .def(py::self * double()) - .def(double() * py::self) + .def(nb::self * double(), nb::is_operator()) + .def(double() * nb::self, nb::is_operator()) // division operators - .def(py::self / double()) - .def("__rtruediv__", [](const sym::Expression& rhs, - double lhs) { return rhs / lhs; }) + .def(nb::self / double(), nb::is_operator()) // comparison operators - .def(py::self == py::self) // NOLINT(misc-redundant-expression) - .def(py::self != py::self) // NOLINT(misc-redundant-expression) - .def(hash(py::self)) + .def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")) + .def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")) + .def(nb::hash(nb::self)) .def("__str__", [](const sym::Expression& expr) { std::stringstream ss; diff --git a/bindings/ir/symbolic/register_term.cpp b/bindings/ir/symbolic/register_term.cpp index 5e9637b769..25b157dff9 100644 --- a/bindings/ir/symbolic/register_term.cpp +++ b/bindings/ir/symbolic/register_term.cpp @@ -10,45 +10,71 @@ #include "ir/operations/Expression.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -// clang-format on - +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerTerm(py::module& m) { - py::class_>(m, "Term") - .def(py::init(), "variable"_a, +void registerTerm(const nb::module_& m) { + nb::class_>( + m, "Term", + R"pb(A symbolic term which consists of a variable with a given coefficient. + +Args: + variable: The variable of the term. + coefficient: The coefficient of the term.)pb") + + .def(nb::init(), "variable"_a, "coefficient"_a = 1.0) - .def_property_readonly("variable", &sym::Term::getVar) - .def_property_readonly("coefficient", &sym::Term::getCoeff) - .def("has_zero_coefficient", &sym::Term::hasZeroCoeff) - .def("add_coefficient", &sym::Term::addCoeff, "coeff"_a) - .def("evaluate", &sym::Term::evaluate, "assignment"_a) - .def(py::self * double()) - .def(double() * py::self) - .def(py::self / double()) - .def(double() / py::self) - .def(py::self == py::self) // NOLINT(misc-redundant-expression) - .def(py::self != py::self) // NOLINT(misc-redundant-expression) - .def(hash(py::self)) + + .def_prop_ro("variable", &sym::Term::getVar, + "The variable of the term.") + + .def_prop_ro("coefficient", &sym::Term::getCoeff, + "The coefficient of the term.") + + .def("has_zero_coefficient", &sym::Term::hasZeroCoeff, + "Check if the coefficient of the term is zero.") + + .def("add_coefficient", &sym::Term::addCoeff, "coeff"_a, + R"pb(Add a coefficient to the coefficient of this term. + +Args: + coeff: The coefficient to add.)pb") + + .def("evaluate", &sym::Term::evaluate, "assignment"_a, + R"pb(Evaluate the term with a given variable assignment. + +Args: + assignment: The variable assignment. + +Returns: + The evaluated value of the term.)pb") + + .def(nb::self * double(), nb::is_operator()) + .def(double() * nb::self, nb::is_operator()) + .def(nb::self / double(), nb::is_operator()) + + .def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")) + .def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")) + .def(nb::hash(nb::self)) + .def("__str__", [](const sym::Term& term) { std::stringstream ss; ss << term; return ss.str(); }) + .def("__repr__", [](const sym::Term& term) { std::stringstream ss; ss << term; diff --git a/bindings/ir/symbolic/register_variable.cpp b/bindings/ir/symbolic/register_variable.cpp index ca7d91405c..93bcc243c2 100644 --- a/bindings/ir/symbolic/register_variable.cpp +++ b/bindings/ir/symbolic/register_variable.cpp @@ -10,33 +10,38 @@ #include "ir/operations/Expression.hpp" -// These includes must be the first includes for any bindings code -// clang-format off -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -// clang-format on - +#include +#include +#include // NOLINT(misc-include-cleaner) #include namespace mqt { -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerVariable(py::module& m) { - py::class_(m, "Variable") - .def(py::init(), "name"_a = "") - .def_property_readonly("name", &sym::Variable::getName) +void registerVariable(const nb::module_& m) { + nb::class_(m, "Variable", R"pb(A symbolic variable. +Note: + Variables are uniquely identified by their name, so if a variable with the same name already exists, the existing variable will be returned. + +Args: + name: The name of the variable.)pb") + + .def(nb::init(), "name"_a = "") + + .def_prop_ro("name", &sym::Variable::getName, "The name of the variable.") + .def("__str__", &sym::Variable::getName) .def("__repr__", &sym::Variable::getName) - .def(py::self == py::self) // NOLINT(misc-redundant-expression) - .def(py::self != py::self) // NOLINT(misc-redundant-expression) - .def(hash(py::self)) - .def(py::self < py::self) // NOLINT(misc-redundant-expression) - .def(py::self > py::self); // NOLINT(misc-redundant-expression) + + .def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")) + .def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")) + .def(nb::hash(nb::self)) + .def(nb::self < nb::self) + .def(nb::self > nb::self); } } // namespace mqt diff --git a/bindings/na/CMakeLists.txt b/bindings/na/CMakeLists.txt index 923f9fb7c5..a7b165059e 100644 --- a/bindings/na/CMakeLists.txt +++ b/bindings/na/CMakeLists.txt @@ -6,13 +6,30 @@ # # Licensed under the MIT License -add_subdirectory(fomac) +set(TARGET_NAME "${MQT_CORE_TARGET_NAME}-na-bindings") -# install the Python stub files in editable mode for better IDE support -if(SKBUILD_STATE STREQUAL "editable") - file(GLOB_RECURSE PYI_FILES ${PROJECT_SOURCE_DIR}/python/mqt/core/na/*.pyi) - install( - FILES ${PYI_FILES} - DESTINATION ./na - COMPONENT ${MQT_CORE_TARGET_NAME}_Python) +if(NOT TARGET ${TARGET_NAME}) + # collect source files + file(GLOB_RECURSE SOURCES *.cpp) + + # declare the Python module + add_mqt_python_binding_nanobind( + CORE + ${TARGET_NAME} + ${SOURCES} + MODULE_NAME + na + INSTALL_DIR + . + LINK_LIBS + MQT::CoreNAFoMaC) + + # install the Python stub file in editable mode for better IDE support + if(SKBUILD_STATE STREQUAL "editable") + file(GLOB_RECURSE NA_PYI_FILES ${PROJECT_SOURCE_DIR}/python/mqt/core/na/*.pyi) + install( + FILES ${NA_PYI_FILES} + DESTINATION ./na + COMPONENT ${MQT_CORE_TARGET_NAME}_Python) + endif() endif() diff --git a/bindings/na/fomac/CMakeLists.txt b/bindings/na/fomac/CMakeLists.txt deleted file mode 100644 index ff31c55551..0000000000 --- a/bindings/na/fomac/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -set(TARGET_NAME "${MQT_CORE_TARGET_NAME}-na-fomac-bindings") - -if(NOT TARGET ${TARGET_NAME}) - # collect source files - file(GLOB_RECURSE SOURCES **.cpp) - - # declare the Python module - add_mqt_python_binding( - CORE - ${TARGET_NAME} - ${SOURCES} - MODULE_NAME - fomac - INSTALL_DIR - ./na - LINK_LIBS - MQT::CoreNAFoMaC) -endif() diff --git a/bindings/na/fomac/fomac.cpp b/bindings/na/fomac/fomac.cpp deleted file mode 100644 index c292f1cf14..0000000000 --- a/bindings/na/fomac/fomac.cpp +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM - * Copyright (c) 2025 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -// These includes must be the first includes for any bindings code -// clang-format off -#include "fomac/FoMaC.hpp" -#include "na/fomac/Device.hpp" -#include "qdmi/na/Generator.hpp" - -#include -#include -#include -#include -#include // NOLINT(misc-include-cleaner) -#include -// clang-format on - -namespace mqt { - -namespace py = pybind11; -using namespace py::literals; - -namespace { -template -concept pyClass = requires(T t) { py::cast(t); }; -template [[nodiscard]] auto repr(T c) -> std::string { - return py::repr(py::cast(c)).template cast(); -} -} // namespace - -// The definition of the (in-)equality operators produces warnings in clang-tidy -// which are ignored by the following comment -// NOLINTBEGIN(misc-redundant-expression) -PYBIND11_MODULE(MQT_CORE_MODULE_NAME, m, py::mod_gil_not_used()) { - pybind11::module_::import("mqt.core.fomac"); - - auto device = - py::class_(m, "Device"); - - auto lattice = py::class_(device, "Lattice"); - - auto vector = py::class_(lattice, "Vector"); - vector.def_readonly("x", &na::Device::Vector::x); - vector.def_readonly("y", &na::Device::Vector::y); - vector.def("__repr__", [](const na::Device::Vector& v) { - return ""; - }); - vector.def(py::self == py::self); - vector.def(py::self != py::self); - - auto region = py::class_(lattice, "Region"); - - auto size = py::class_(region, "Size"); - size.def_readonly("width", &na::Device::Region::Size::width); - size.def_readonly("height", &na::Device::Region::Size::height); - size.def("__repr__", [](const na::Device::Region::Size& s) { - return ""; - }); - size.def(py::self == py::self); - size.def(py::self != py::self); - - region.def_readonly("origin", &na::Device::Region::origin); - region.def_readonly("size", &na::Device::Region::size); - region.def("__repr__", [](const na::Device::Region& r) { - return ""; - }); - region.def(py::self == py::self); - region.def(py::self != py::self); - - lattice.def_readonly("lattice_origin", &na::Device::Lattice::latticeOrigin); - lattice.def_readonly("lattice_vector_1", - &na::Device::Lattice::latticeVector1); - lattice.def_readonly("lattice_vector_2", - &na::Device::Lattice::latticeVector2); - lattice.def_readonly("sublattice_offsets", - &na::Device::Lattice::sublatticeOffsets); - lattice.def_readonly("extent", &na::Device::Lattice::extent); - lattice.def("__repr__", [](const na::Device::Lattice& l) { - return ""; - }); - lattice.def(py::self == py::self); - lattice.def(py::self != py::self); - - device.def_property_readonly("traps", &na::Session::Device::getTraps); - device.def_property_readonly("t1", [](const na::Session::Device& dev) { - return dev.getDecoherenceTimes().t1; - }); - device.def_property_readonly("t2", [](const na::Session::Device& dev) { - return dev.getDecoherenceTimes().t2; - }); - device.def("__repr__", [](const fomac::Session::Device& dev) { - return ""; - }); - device.def(py::self == py::self); - device.def(py::self != py::self); - - m.def("devices", &na::Session::getDevices); - device.def_static("try_create_from_device", - &na::Session::Device::tryCreateFromDevice, "device"_a); -} -// NOLINTEND(misc-redundant-expression) -} // namespace mqt diff --git a/bindings/na/register_fomac.cpp b/bindings/na/register_fomac.cpp new file mode 100644 index 0000000000..864ad9a120 --- /dev/null +++ b/bindings/na/register_fomac.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "fomac/FoMaC.hpp" +#include "na/fomac/Device.hpp" +#include "qdmi/na/Generator.hpp" + +#include +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include + +namespace mqt { + +namespace nb = nanobind; +using namespace nb::literals; + +namespace { + +template +concept pyClass = requires(T t) { nb::cast(t); }; +template [[nodiscard]] auto repr(T c) -> std::string { + return nb::repr(nb::cast(c)).c_str(); +} + +} // namespace + +// NOLINTNEXTLINE(misc-use-internal-linkage) +void registerFomac(nb::module_& m) { + nb::module_::import_("mqt.core.fomac"); + + auto device = nb::class_( + m, "Device", "Represents a device with a lattice of traps."); + + auto lattice = nb::class_( + device, "Lattice", "Represents a lattice of traps in the device."); + + auto vector = nb::class_(lattice, "Vector", + "Represents a 2D vector."); + vector.def_ro("x", &na::Device::Vector::x, "The x-coordinate of the vector."); + vector.def_ro("y", &na::Device::Vector::y, "The y-coordinate of the vector."); + vector.def("__repr__", [](const na::Device::Vector& v) { + return ""; + }); + vector.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + vector.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); + + auto region = nb::class_( + lattice, "Region", "Represents a region in the device."); + + auto size = nb::class_( + region, "Size", "Represents the size of a region."); + size.def_ro("width", &na::Device::Region::Size::width, + "The width of the region."); + size.def_ro("height", &na::Device::Region::Size::height, + "The height of the region."); + size.def("__repr__", [](const na::Device::Region::Size& s) { + return ""; + }); + size.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + size.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); + + region.def_ro("origin", &na::Device::Region::origin, + "The origin of the region."); + region.def_ro("size", &na::Device::Region::size, "The size of the region."); + region.def("__repr__", [](const na::Device::Region& r) { + return ""; + }); + region.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + region.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); + + lattice.def_ro("lattice_origin", &na::Device::Lattice::latticeOrigin, + "The origin of the lattice."); + lattice.def_ro("lattice_vector_1", &na::Device::Lattice::latticeVector1, + "The first lattice vector."); + lattice.def_ro("lattice_vector_2", &na::Device::Lattice::latticeVector2, + "The second lattice vector."); + lattice.def_ro("sublattice_offsets", &na::Device::Lattice::sublatticeOffsets, + "The offsets of the sublattices."); + lattice.def_ro("extent", &na::Device::Lattice::extent, + "The extent of the lattice."); + lattice.def("__repr__", [](const na::Device::Lattice& l) { + return ""; + }); + lattice.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + lattice.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); + + device.def_prop_ro("traps", &na::Session::Device::getTraps, + nb::rv_policy::reference_internal, + "The list of trap positions in the device."); + device.def_prop_ro( + "t1", + [](const na::Session::Device& dev) { + return dev.getDecoherenceTimes().t1; + }, + "The T1 time of the device."); + device.def_prop_ro( + "t2", + [](const na::Session::Device& dev) { + return dev.getDecoherenceTimes().t2; + }, + "The T2 time of the device."); + device.def("__repr__", [](const fomac::Session::Device& dev) { + return ""; + }); + device.def_static("try_create_from_device", + &na::Session::Device::tryCreateFromDevice, "device"_a, + R"pb(Create NA FoMaC Device from generic FoMaC Device. + +Args: + device: The generic FoMaC Device to convert. + +Returns: + The converted NA FoMaC Device or None if the conversion is not possible.)pb"); + device.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool")); + device.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool")); + + m.def("devices", &na::Session::getDevices, nb::rv_policy::reference_internal, + "Returns a list of available devices."); +} + +} // namespace mqt diff --git a/bindings/na/register_na.cpp b/bindings/na/register_na.cpp new file mode 100644 index 0000000000..0a8a7a145f --- /dev/null +++ b/bindings/na/register_na.cpp @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include + +namespace mqt { + +namespace nb = nanobind; + +// forward declarations +void registerFomac(nb::module_& m); + +NB_MODULE(MQT_CORE_MODULE_NAME, m) { + m.doc() = R"pb(MQT Core NA - The MQT Core neutral atom module. + +This module contains all neutral atom related functionality of MQT Core.)pb"; + + nb::module_ fomac = m.def_submodule("fomac"); + registerFomac(fomac); +} + +} // namespace mqt diff --git a/cmake/AddMQTPythonBinding.cmake b/cmake/AddMQTPythonBinding.cmake index f75be0808c..283b58b5b4 100644 --- a/cmake/AddMQTPythonBinding.cmake +++ b/cmake/AddMQTPythonBinding.cmake @@ -53,3 +53,50 @@ function(add_mqt_python_binding package_name target_name) DESTINATION ${ARG_INSTALL_DIR} COMPONENT ${MQT_${package_name}_TARGET_NAME}_Python) endfunction() + +function(add_mqt_python_binding_nanobind package_name target_name) + cmake_parse_arguments(ARG "" "MODULE_NAME;INSTALL_DIR" "LINK_LIBS" ${ARGN}) + set(SOURCES ${ARG_UNPARSED_ARGUMENTS}) + + nanobind_add_module( + # Name of the extension + ${target_name} + # Target the stable ABI for Python 3.12+, which reduces the number of binary wheels + STABLE_ABI + # Enable free-threaded support + FREE_THREADED + # Suppress compiler warnings from the nanobind library + NB_SUPPRESS_WARNINGS + # Source files + ${SOURCES}) + + # Set C++ standard + target_compile_features(${target_name} PRIVATE cxx_std_20) + + if(ARG_MODULE_NAME) + # The library name must be the same as the module name + set_target_properties(${target_name} PROPERTIES OUTPUT_NAME ${ARG_MODULE_NAME}) + target_compile_definitions(${target_name} + PRIVATE MQT_${package_name}_MODULE_NAME=${ARG_MODULE_NAME}) + else() + # Use the target name as the module name + target_compile_definitions(${target_name} + PRIVATE MQT_${package_name}_MODULE_NAME=${target_name}) + endif() + + # Add project libraries to the link libraries + list(APPEND ARG_LINK_LIBS MQT::ProjectOptions MQT::ProjectWarnings) + + target_link_libraries(${target_name} PRIVATE ${ARG_LINK_LIBS}) + + # Set default "." for INSTALL_DIR + if(NOT ARG_INSTALL_DIR) + set(ARG_INSTALL_DIR ".") + endif() + + # Install directive for scikit-build-core + install( + TARGETS ${target_name} + DESTINATION ${ARG_INSTALL_DIR} + COMPONENT ${MQT_${package_name}_TARGET_NAME}_Python) +endfunction() diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index d96db70872..07da29d47a 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -13,19 +13,12 @@ include(CMakeDependentOption) set(FETCH_PACKAGES "") if(BUILD_MQT_CORE_BINDINGS) - if(NOT SKBUILD) - # Manually detect the installed pybind11 package and import it into CMake. - execute_process( - COMMAND "${Python_EXECUTABLE}" -m pybind11 --cmakedir - OUTPUT_STRIP_TRAILING_WHITESPACE - OUTPUT_VARIABLE pybind11_DIR) - list(APPEND CMAKE_PREFIX_PATH "${pybind11_DIR}") - endif() - - message(STATUS "Python executable: ${Python_EXECUTABLE}") - - # add pybind11 library - find_package(pybind11 3.0.1 CONFIG REQUIRED) + # Detect the installed nanobind package and import it into CMake + execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + OUTPUT_VARIABLE nanobind_ROOT) + find_package(nanobind CONFIG REQUIRED) endif() set(JSON_VERSION diff --git a/docs/qdmi/driver.md b/docs/qdmi/driver.md index ed836dae88..906c6cd390 100644 --- a/docs/qdmi/driver.md +++ b/docs/qdmi/driver.md @@ -17,7 +17,7 @@ Other devices can be loaded dynamically at runtime via {cpp:func}`qdmi::Driver:: ## Python Bindings -The QDMI Driver is implemented in C++ and exposed to Python via [pybind11](https://pybind11.readthedocs.io). +The QDMI Driver is implemented in C++ and exposed to Python via [{code}`nanobind`](https://nanobind.readthedocs.io/). Direct binding of the QDMI Client interface functions is not feasible due to technical limitations. Instead, a FoMaC (Figure of Merits and Constraints) library defines wrapper classes ({cpp:class}`~fomac::Session`, {cpp:class}`~fomac::Session::Device`, {cpp:class}`~fomac::Session::Device::Site`, {cpp:class}`~fomac::Session::Device::Operation`, {cpp:class}`~fomac::Session::Job`) for the QDMI entities. These classes together with their methods are then exposed to Python, see {py:class}`~mqt.core.fomac.Session`, {py:class}`~mqt.core.fomac.Device`, {py:class}`~mqt.core.fomac.Device.Site`, {py:class}`~mqt.core.fomac.Device.Operation`, {py:class}`~mqt.core.fomac.Job`. diff --git a/include/mqt-core/ir/operations/Expression.hpp b/include/mqt-core/ir/operations/Expression.hpp index 47a7ce230b..b8aa30f828 100644 --- a/include/mqt-core/ir/operations/Expression.hpp +++ b/include/mqt-core/ir/operations/Expression.hpp @@ -255,11 +255,6 @@ template operator*(double lhs, const Term& rhs) { return rhs * lhs; } -template >> -Term operator/(double lhs, const Term& rhs) { - return rhs / lhs; -} ///@} /** diff --git a/include/mqt-core/ir/operations/IfElseOperation.hpp b/include/mqt-core/ir/operations/IfElseOperation.hpp index 225db1e2b0..761b2150ae 100644 --- a/include/mqt-core/ir/operations/IfElseOperation.hpp +++ b/include/mqt-core/ir/operations/IfElseOperation.hpp @@ -66,28 +66,28 @@ class IfElseOperation final : public Operation { [[nodiscard]] bool isControlled() const override { return false; } - [[nodiscard]] auto getThenOp() const { return thenOp.get(); } + [[nodiscard]] auto getThenOp() const { return thenOp_.get(); } - [[nodiscard]] auto getElseOp() const { return elseOp.get(); } + [[nodiscard]] auto getElseOp() const { return elseOp_.get(); } [[nodiscard]] const auto& getControlRegister() const noexcept { - return controlRegister; + return controlRegister_; } [[nodiscard]] const auto& getControlBit() const noexcept { - return controlBit; + return controlBit_; } [[nodiscard]] auto getExpectedValueRegister() const noexcept { - return expectedValueRegister; + return expectedValueRegister_; } [[nodiscard]] bool getExpectedValueBit() const noexcept { - return expectedValueBit; + return expectedValueBit_; } [[nodiscard]] auto getComparisonKind() const noexcept { - return comparisonKind; + return comparisonKind_; } [[nodiscard]] bool equals(const Operation& op, const Permutation& perm1, @@ -109,27 +109,27 @@ class IfElseOperation final : public Operation { } // Override invalid Operation setters - void setTargets(const Targets&) override { + void setTargets(const Targets& /*t*/) override { throw std::runtime_error("An IfElseOperation does not have a target."); } - void setControls(const Controls&) override { + void setControls(const Controls& /*c*/) override { throw std::runtime_error("An IfElseOperation cannot be controlled."); } - void addControl(Control) override { + void addControl(Control /*c*/) override { throw std::runtime_error("An IfElseOperation cannot be controlled."); } void clearControls() override { throw std::runtime_error("An IfElseOperation cannot be controlled."); } - void removeControl(Control) override { + void removeControl(Control /*c*/) override { throw std::runtime_error("An IfElseOperation cannot be controlled."); } - Controls::iterator removeControl(Controls::iterator) override { + Controls::iterator removeControl(Controls::iterator /*it*/) override { throw std::runtime_error("An IfElseOperation cannot be controlled."); } - void setGate(const OpType) override { + void setGate(const OpType /*g*/) override { throw std::runtime_error( "Cannot set operation type of an IfElseOperation."); } @@ -139,13 +139,13 @@ class IfElseOperation final : public Operation { } private: - std::unique_ptr thenOp; - std::unique_ptr elseOp; - std::optional controlRegister; - std::optional controlBit; - std::uint64_t expectedValueRegister = 1U; - bool expectedValueBit = true; - ComparisonKind comparisonKind = Eq; + std::unique_ptr thenOp_; + std::unique_ptr elseOp_; + std::optional controlRegister_; + std::optional controlBit_; + std::uint64_t expectedValueRegister_ = 1U; + bool expectedValueBit_ = true; + ComparisonKind comparisonKind_ = Eq; /** * @brief Canonicalizes the IfElseOperation @@ -162,8 +162,6 @@ class IfElseOperation final : public Operation { }; } // namespace qc -namespace std { -template <> struct hash { +template <> struct std::hash { std::size_t operator()(qc::IfElseOperation const& op) const noexcept; -}; -} // namespace std +}; // namespace std diff --git a/noxfile.py b/noxfile.py index 946ad48252..694ffd3826 100755 --- a/noxfile.py +++ b/noxfile.py @@ -207,5 +207,56 @@ 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" / "core" + pattern_file = Path(__file__).parent / "bindings" / "core_patterns.txt" + + session.run( + "python", + "-m", + "nanobind.stubgen", + "--recursive", + "--include-private", + "--output-dir", + str(package_root), + "--pattern-file", + str(pattern_file), + "--module", + "mqt.core.ir", + "--module", + "mqt.core.dd", + "--module", + "mqt.core.fomac", + "--module", + "mqt.core.na", + ) + + pyi_files = list(package_root.glob("**/*.pyi")) + + 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) + + # Finally, run ruff-check again to ensure everything is clean. + session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True) + + if __name__ == "__main__": nox.main() diff --git a/pyproject.toml b/pyproject.toml index 29727c66f5..f94599ace7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,11 @@ # Licensed under the MIT License [build-system] -requires = ["scikit-build-core>=0.11.6", "setuptools-scm>=9.2.2", "pybind11>=3.0.1"] +requires = [ + "nanobind>=2.10.2", + "scikit-build-core>=0.11.6", + "setuptools-scm>=9.2.2", +] build-backend = "scikit_build_core.build" [project] @@ -69,6 +73,9 @@ wheel.install-dir = "mqt/core" # 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" @@ -89,8 +96,7 @@ build.targets = [ "mqt-core-qdmi-na-device", "mqt-core-qdmi-ddsim-device", "mqt-core-fomac-bindings", - "mqt-core-na-fomac", - "mqt-core-na-fomac-bindings", + "mqt-core-na-bindings", ] metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" @@ -225,8 +231,8 @@ known-first-party = ["mqt.core"] [tool.ruff.lint.per-file-ignores] "test/python/**" = ["T20", "INP001"] "docs/**" = ["T20", "INP001"] -"noxfile.py" = ["T20", "TID251"] -"*.pyi" = ["D418", "DOC202", "PYI011", "PYI021"] +"noxfile.py" = ["T20", "TID251", "PLC0415"] +"*.pyi" = ["D418", "E501", "PYI021"] "*.ipynb" = [ "D", # pydocstyle "E402", # Allow imports to appear anywhere in Jupyter notebooks @@ -256,6 +262,7 @@ anc = "anc" mch = "mch" ket = "ket" optin = "optin" +nd = "nd" [tool.repo-review] @@ -288,6 +295,11 @@ environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" } before-build = "uv pip install delvewheel>=1.11.2" repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel} --namespace-pkg mqt --ignore-existing" +[[tool.cibuildwheel.overrides]] +select = "cp312-*" +inherit.repair-wheel-command = "append" +repair-wheel-command = "uvx abi3audit --strict --report {wheel}" + [tool.uv] required-version = ">=0.5.20" @@ -311,7 +323,7 @@ exclude = [ [dependency-groups] build = [ - "pybind11>=3.0.1", + "nanobind>=2.10.2", "scikit-build-core>=0.11.6", "setuptools-scm>=9.2.2", ] diff --git a/python/mqt/core/dd.pyi b/python/mqt/core/dd.pyi index 40c5021ff1..3a8f6b9ab9 100644 --- a/python/mqt/core/dd.pyi +++ b/python/mqt/core/dd.pyi @@ -6,181 +6,200 @@ # # Licensed under the MIT License -"""MQT Core DD - The MQT Decision Diagram Package.""" - -from collections.abc import Iterable -from enum import Enum +import enum +from collections.abc import Sequence +from collections.abc import Set as AbstractSet +from typing import Annotated import numpy as np -import numpy.typing as npt - -from .ir import Permutation, QuantumComputation -from .ir.operations import Control, IfElseOperation, NonUnitaryOperation, Operation - -__all__ = [ - "BasisStates", - "DDPackage", - "Matrix", - "MatrixDD", - "Vector", - "VectorDD", - "build_functionality", - "build_unitary", - "sample", - "simulate", - "simulate_statevector", -] - -def sample(qc: QuantumComputation, shots: int = 1024, seed: int = 0) -> dict[str, int]: - """Sample from the output distribution of a quantum computation. +from numpy.typing import NDArray - This function classically simulates the quantum computation and repeatedly - samples from the output distribution. It supports mid-circuit measurements, - resets, and classical control. +import mqt.core.ir +import mqt.core.ir.operations - Args: - qc: The quantum computation. - shots: The number of samples to take. If the quantum computation contains - no mid-circuit measurements or resets, the circuit is simulated - once and the samples are drawn from the final state. Otherwise, - the circuit is simulated once for each sample. Defaults to 1024. - seed: The seed for the random number generator. If set to a specific - non-zero value, the simulation is deterministic. If set to 0, the - RNG is randomly seeded. Defaults to 0. +class VectorDD: + """A class representing a vector decision diagram (DD).""" - Returns: - A histogram of the samples. Each sample is a bitstring representing the - measurement outcomes of the qubits in the quantum computation. The - leftmost bit corresponds to the most significant qubit, that is, the - qubit with the highest index (big-endian). If the circuit contains - measurements, only the qubits that are actively measured are included in - the output distribution. Otherwise, all qubits in the circuit are measured. - """ + def is_terminal(self) -> bool: + """Check if the DD is a terminal node.""" -def simulate_statevector(qc: QuantumComputation) -> Vector: - """Simulate the quantum computation and return the final state vector. + def is_zero_terminal(self) -> bool: + """Check if the DD is a zero terminal node.""" - This function classically simulates the quantum computation and returns the - state vector of the final state. - It does not support measurements, resets, or classical control. + def size(self) -> int: + """Get the size of the DD by traversing it once.""" - Since the state vector is guaranteed to be exponentially large in the number - of qubits, this function is only suitable for small quantum computations. - Consider using the :func:`~mqt.core.dd.simulate` or the - :func:`~mqt.core.dd.sample` functions, which never explicitly construct - the state vector, for larger quantum computations. + def __getitem__(self, key: int) -> complex: + """Get the amplitude of a basis state by index.""" - Args: - qc: The quantum computation. - Must only contain unitary operations. + def get_amplitude(self, num_qubits: int, decisions: str) -> complex: + """Get the amplitude of a basis state by decisions. - Returns: - The state vector of the final state. + Args: + num_qubits: The number of qubits. + decisions: The decisions as a string of bits (`0` or `1`), where `decisions[i]` corresponds to the successor to follow at level `i` of the DD. + Must be at least `num_qubits` long. - Notes: - This function internally constructs a :class:`~mqt.core.dd.DDPackage`, creates the - zero state, and simulates the quantum computation via the :func:`simulate` - function. - The state vector is then extracted from the resulting DD via the :meth:`~mqt.core.dd.VectorDD.get_vector` - method. - The resulting :class:`~mqt.core.dd.Vector` can be converted to a NumPy array without copying - the data by calling :func:`numpy.array` with the `copy=False` argument. - """ + Returns: + The amplitude of the basis state. + """ -def build_unitary(qc: QuantumComputation, recursive: bool = False) -> Matrix: - """Build a unitary matrix representation of a quantum computation. + def get_vector(self, threshold: float = 0.0) -> Annotated[NDArray[np.complex128], {"shape": (None,)}]: + """Get the state vector represented by the DD. - This function builds a matrix representation of the unitary representing the - functionality of a quantum computation. - This function does not support measurements, resets, or classical control, - as the corresponding operations are non-unitary. + Args: + threshold: The threshold for not including amplitudes in the state vector. Defaults to 0.0. - Since the unitary matrix is guaranteed to be exponentially large in the number - of qubits, this function is only suitable for small quantum computations. - Consider using the :func:`~mqt.core.dd.build_functionality` function, which - never explicitly constructs the unitary matrix, for larger quantum computations. + Returns: + The state vector. - Args: - qc: The quantum computation. - Must only contain unitary operations. - recursive: Whether to build the unitary matrix recursively. - If set to True, the unitary matrix is built recursively by - pairwise grouping the operations of the quantum computation. - If set to False, the unitary matrix is built by sequentially - applying the operations of the quantum computation to the - identity matrix. - Defaults to False. + Raises: + MemoryError: If the memory allocation fails. + """ - Returns: - The unitary matrix representing the functionality of the quantum computation. + def to_dot( + self, + colored: bool = True, + edge_labels: bool = False, + classic: bool = False, + memory: bool = False, + format_as_polar: bool = True, + ) -> str: + """Convert the DD to a DOT graph that can be plotted via Graphviz. - Notes: - This function internally constructs a :class:`~mqt.core.dd.DDPackage`, creates the - identity matrix, and builds the unitary matrix via the :func:`~mqt.core.dd.build_functionality` - function. - The unitary matrix is then extracted from the resulting DD via the :meth:`~mqt.core.dd.MatrixDD.get_matrix` - method. - The resulting :class:`~mqt.core.dd.Matrix` can be converted to a NumPy array without copying - the data by calling :func:`numpy.array` with the `copy=False` argument. - """ + Args: + colored: Whether to use colored edge weights + edge_labels: Whether to include edge weights as labels. + classic: Whether to use the classic DD visualization style. + memory: Whether to include memory information. For debugging purposes only. + format_as_polar: Whether to format the edge weights in polar coordinates. -def simulate(qc: QuantumComputation, initial_state: VectorDD, dd_package: DDPackage) -> VectorDD: - """Simulate a quantum computation. + Returns: + The DOT graph. + """ - This function classically simulates a quantum computation for a given initial - state and returns the final state (represented as a DD). Compared to the - `sample` function, this function does not support measurements, resets, or - classical control. It only supports unitary operations. + def to_svg( + self, + filename: str, + colored: bool = True, + edge_labels: bool = False, + classic: bool = False, + memory: bool = False, + format_as_polar: bool = True, + ) -> None: + """Convert the DD to an SVG file that can be viewed in a browser. - The simulation is effectively computed by sequentially applying the operations - of the quantum computation to the initial state. + Requires the `dot` command from Graphviz to be installed and available in the PATH. - Args: - qc: The quantum computation. - Must only contain unitary operations. - initial_state: The initial state as a DD. Must have the same number of qubits - as the quantum computation. The reference count of the initial - state is decremented during the simulation, so the caller must - ensure that the initial state has a non-zero reference count. - dd_package: The DD package. Must be configured with a sufficient number of - qubits to accommodate the quantum computation. + Args: + filename: The filename of the SVG file. Any file extension will be replaced by `.dot` and then `.svg`. + colored: Whether to use colored edge weights. + edge_labels: Whether to include edge weights as labels. + classic: Whether to use the classic DD visualization style. + memory: Whether to include memory information. For debugging purposes only. + format_as_polar: Whether to format the edge weights in polar coordinates. + """ - Returns: - The final state as a DD. The reference count of the final state is non-zero - and must be manually decremented by the caller if it is no longer needed. - """ +class MatrixDD: + """A class representing a matrix decision diagram (DD).""" -def build_functionality(qc: QuantumComputation, dd_package: DDPackage, recursive: bool = False) -> MatrixDD: - """Build a functional representation of a quantum computation. + def is_terminal(self) -> bool: + """Check if the DD is a terminal node.""" - This function builds a matrix DD representation of the unitary representing - the functionality of a quantum computation. This function does not support - measurements, resets, or classical control, as the corresponding operations - are non-unitary. + def is_zero_terminal(self) -> bool: + """Check if the DD is a zero terminal node.""" - Args: - qc: The quantum computation. - Must only contain unitary operations. - dd_package: The DD package. - Must be configured with a sufficient number of qubits to - accommodate the quantum computation. - recursive: Whether to build the functionality matrix recursively. If set - to True, the functionality matrix is built recursively by - pairwise grouping the operations of the quantum computation. - If set to False, the functionality matrix is built by - sequentially applying the operations of the quantum - computation to the identity matrix. Defaults to False. + def is_identity(self, up_to_global_phase: bool = True) -> bool: + """Check if the DD represents the identity matrix. - Returns: - The functionality as a DD. The reference count of the result is non-zero - and must be manually decremented by the caller if it is no longer needed. - """ + Args: + up_to_global_phase: Whether to ignore global phase. + + Returns: + Whether the DD represents the identity matrix. + """ + + def size(self) -> int: + """Get the size of the DD by traversing it once.""" + + def get_entry(self, num_qubits: int, row: int, col: int) -> complex: + """Get the entry of the matrix by row and column index.""" + + def get_entry_by_path(self, num_qubits: int, decisions: str) -> complex: + """Get the entry of the matrix by decisions. + + Args: + num_qubits: The number of qubits. + decisions: The decisions as a string of `0`, `1`, `2`, or `3`, where `decisions[i]` corresponds to the successor to follow at level `i` of the DD. + Must be at least `num_qubits` long. + + Returns: + The entry of the matrix. + """ + + def get_matrix( + self, num_qubits: int, threshold: float = 0.0 + ) -> Annotated[NDArray[np.complex128], {"shape": (None, None)}]: + """Get the matrix represented by the DD. + + Args: + num_qubits: The number of qubits. + threshold: The threshold for not including entries in the matrix. Defaults to 0.0. + + Returns: + The matrix. + + Raises: + MemoryError: If the memory allocation fails. + """ + + def to_dot( + self, + colored: bool = True, + edge_labels: bool = False, + classic: bool = False, + memory: bool = False, + format_as_polar: bool = True, + ) -> str: + """Convert the DD to a DOT graph that can be plotted via Graphviz. + + Args: + colored: Whether to use colored edge weights + edge_labels: Whether to include edge weights as labels. + classic: Whether to use the classic DD visualization style. + memory: Whether to include memory information. For debugging purposes only. + format_as_polar: Whether to format the edge weights in polar coordinates. + + Returns: + The DOT graph. + """ + + def to_svg( + self, + filename: str, + colored: bool = True, + edge_labels: bool = False, + classic: bool = False, + memory: bool = False, + format_as_polar: bool = True, + ) -> None: + """Convert the DD to an SVG file that can be viewed in a browser. + + Requires the `dot` command from Graphviz to be installed and available in the PATH. + + Args: + filename: The filename of the SVG file. Any file extension will be replaced by `.dot` and then `.svg`. + colored: Whether to use colored edge weights. + edge_labels: Whether to include edge weights as labels. + classic: Whether to use the classic DD visualization style. + memory: Whether to include memory information. For debugging purposes only. + format_as_polar: Whether to format the edge weights in polar coordinates. + """ class DDPackage: """The central manager for performing computations on decision diagrams. - It drives all computation on decision diagrams and maintains the necessary - data structures for this purpose. + It drives all computation on decision diagrams and maintains the necessary data structures for this purpose. Specifically, it - manages the memory for the decision diagram nodes (Memory Manager), @@ -190,19 +209,16 @@ class DDPackage: - provides methods for various operations on quantum states and operations, and - provides means for reference counting and garbage collection. - Args: - num_qubits: The maximum number of qubits that the DDPackage can handle. - Mainly influences the size of the unique tables. - Can be adjusted dynamically using the `resize` method. - Since resizing the DDPackage can be expensive, it is recommended - to choose a value that is large enough for the quantum computations - that are to be performed, but not unnecessarily large. - Default is 32. - Notes: - It is undefined behavior to pass VectorDD or MatrixDD objects that were - created with a different DDPackage to the methods of the DDPackage. + It is undefined behavior to pass VectorDD or MatrixDD objects that were created with a different DDPackage to the methods of the DDPackage. + The only exception is the identity DD returned by identity(), which represents the global one-terminal and can be used with any DDPackage instance. + Args: + num_qubits: The maximum number of qubits that the DDPackage can handle. + Mainly influences the size of the unique tables. + Can be adjusted dynamically using the `resize` method. + Since resizing the DDPackage can be expensive, it is recommended to choose a value that is large enough for the quantum computations that are to be performed, but not unnecessarily large. + Default is 32. """ def __init__(self, num_qubits: int = 32) -> None: ... @@ -211,11 +227,8 @@ class DDPackage: Args: num_qubits: The new number of qubits. - Must be greater than zero. - It is undefined behavior to resize the DDPackage to a - smaller number of qubits and then perform operations - on decision diagrams that are associated with qubits - that are no longer present. + Must be greater than zero. + It is undefined behavior to resize the DDPackage to a smaller number of qubits and then perform operations on decision diagrams that are associated with qubits that are no longer present. """ @property @@ -223,39 +236,39 @@ class DDPackage: """The maximum number of qubits that the DDPackage can handle.""" def zero_state(self, num_qubits: int) -> VectorDD: - r"""Create the DD for the zero state :math:`|0\ldots 0\rangle`. + r"""Create the DD for the zero state :math:`| 0 \ldots 0 \rangle`. Args: num_qubits: The number of qubits. - Must not be greater than the number of qubits the DDPackage is configured with. + Must not be greater than the number of qubits the DDPackage is configured with. Returns: The DD for the zero state. The resulting state is guaranteed to have its reference count increased. """ - def computational_basis_state(self, num_qubits: int, state: list[bool]) -> VectorDD: - r"""Create the DD for the computational basis state :math:`|b_{n-1} \ldots b_0\rangle`. + def computational_basis_state(self, num_qubits: int, state: Sequence[bool]) -> VectorDD: + r"""Create the DD for the computational basis state :math:`| b_{n - 1} \ldots b_0 \rangle`. Args: num_qubits: The number of qubits. - Must not be greater than the number of qubits the DDPackage is configured with. + Must not be greater than the number of qubits the DDPackage is configured with. state: The state as a list of booleans. - Must be at least `num_qubits` long. + Must be at least `num_qubits` long. Returns: The DD for the computational basis state. The resulting state is guaranteed to have its reference count increased. """ - def basis_state(self, num_qubits: int, state: Iterable[BasisStates]) -> VectorDD: - r"""Create the DD for the basis state :math:`|B_{n-1} \ldots B_0\rangle`, where :math:`B_i\in\{0,1,+\,-,L,R\}`. + def basis_state(self, num_qubits: int, state: Sequence[BasisStates]) -> VectorDD: + r"""Create the DD for the basis state :math:`| B_{n - 1} \ldots B_0 \rangle`, where :math:`B_i \in \{0, 1, +\, -\, L, R\}`. Args: num_qubits: The number of qubits. - Must not be greater than the number of qubits the DDPackage is configured with. + Must not be greater than the number of qubits the DDPackage is configured with. state: The state as an iterable of :class:`BasisStates`. - Must be at least `num_qubits` long. + Must be at least `num_qubits` long. Returns: The DD for the basis state. @@ -263,11 +276,11 @@ class DDPackage: """ def ghz_state(self, num_qubits: int) -> VectorDD: - r"""Create the DD for the GHZ state :math:`\frac{1}{\sqrt{2}}(|0\ldots 0\rangle + |1\ldots 1\rangle)`. + r"""Create the DD for the GHZ state :math:`\frac{1}{\sqrt{2}} (| 0 \ldots 0 \rangle + |1 \ldots 1 \rangle)`. Args: num_qubits: The number of qubits. - Must not be greater than the number of qubits the DDPackage is configured with. + Must not be greater than the number of qubits the DDPackage is configured with. Returns: The DD for the GHZ state. @@ -277,98 +290,106 @@ class DDPackage: def w_state(self, num_qubits: int) -> VectorDD: r"""Create the DD for the W state :math:`|W\rangle`. - :math:`|W\rangle = \frac{1}{\sqrt{n}}(|100\ldots 0\rangle + |010\ldots 0\rangle + \ldots + |000\ldots 1\rangle)` + .. math:: + |W\rangle = \frac{1}{\sqrt{n}} (| 100 \ldots 0 \rangle + | 010 \ldots 0 \rangle + \ldots + | 000 \ldots 1 \rangle) Args: num_qubits: The number of qubits. - Must not be greater than the number of qubits the DDPackage is configured with. + Must not be greater than the number of qubits the DDPackage is configured with. Returns: The DD for the W state. The resulting state is guaranteed to have its reference count increased. """ - def from_vector(self, state: npt.NDArray[np.cdouble]) -> VectorDD: + def from_vector(self, state: Annotated[NDArray[np.complex128], {"shape": (None,)}]) -> VectorDD: """Create a DD from a state vector. Args: state: The state vector. - Must have a length that is a power of 2. - Must not require more qubits than the DDPackage is configured with. + Must have a length that is a power of 2. + Must not require more qubits than the DDPackage is configured with. Returns: The DD for the vector. The resulting state is guaranteed to have its reference count increased. """ - def apply_unitary_operation(self, vec: VectorDD, operation: Operation, permutation: Permutation = ...) -> VectorDD: + def apply_unitary_operation( + self, vec: VectorDD, operation: mqt.core.ir.operations.Operation, permutation: mqt.core.ir.Permutation = ... + ) -> VectorDD: """Apply a unitary operation to the DD. + Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + Args: vec: The input DD. - operation: The operation. - Must be unitary. - permutation: The permutation of the qubits. - Defaults to the identity permutation. + operation: The operation. Must be unitary. + permutation: The permutation of the qubits. Defaults to the identity permutation. Returns: The resulting DD. - - Notes: - Automatically manages the reference count of the input and output DDs. - The input DD must have a non-zero reference count. """ def apply_measurement( self, vec: VectorDD, - operation: NonUnitaryOperation, - measurements: list[bool], - permutation: Permutation = ..., + operation: mqt.core.ir.operations.NonUnitaryOperation, + measurements: Sequence[bool], + permutation: mqt.core.ir.Permutation = ..., ) -> tuple[VectorDD, list[bool]]: """Apply a measurement to the DD. + Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count + Args: vec: The input DD. operation: The measurement operation. measurements: A list of bits with existing measurement outcomes. - permutation: The permutation of the qubits. - Defaults to the identity permutation. + permutation: The permutation of the qubits. Defaults to the identity permutation. Returns: The resulting DD after the measurement as well as the updated measurement outcomes. + """ + + def apply_reset( + self, + vec: VectorDD, + operation: mqt.core.ir.operations.NonUnitaryOperation, + permutation: mqt.core.ir.Permutation = ..., + ) -> VectorDD: + """Apply a reset to the DD. Notes: Automatically manages the reference count of the input and output DDs. The input DD must have a non-zero reference count. - """ - - def apply_reset(self, vec: VectorDD, operation: NonUnitaryOperation, permutation: Permutation = ...) -> VectorDD: - """Apply a reset to the DD. Args: vec: The input DD. operation: The reset operation. - permutation: The permutation of the qubits. - Defaults to the identity permutation. + permutation: The permutation of the qubits. Defaults to the identity permutation. Returns: The resulting DD after the reset. - - Notes: - Automatically manages the reference count of the input and output DDs. - The input DD must have a non-zero reference count. """ def apply_if_else_operation( self, vec: VectorDD, - operation: IfElseOperation, - measurements: list[bool], - permutation: Permutation = ..., + operation: mqt.core.ir.operations.IfElseOperation, + measurements: Sequence[bool], + permutation: mqt.core.ir.Permutation = ..., ) -> VectorDD: """Apply a classically controlled operation to the DD. + Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + Args: vec: The input DD. operation: The classically controlled operation. @@ -377,59 +398,54 @@ class DDPackage: Returns: The resulting DD after the operation. - - Notes: - Automatically manages the reference count of the input and output DDs. - The input DD must have a non-zero reference count. """ def measure_collapsing(self, vec: VectorDD, qubit: int) -> str: """Measure a qubit and collapse the DD. + Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + Args: vec: The input DD. qubit: The qubit to measure. Returns: The measurement outcome. - - Notes: - Automatically manages the reference count of the input and output DDs. - The input DD must have a non-zero reference count. """ def measure_all(self, vec: VectorDD, collapse: bool = False) -> str: """Measure all qubits. + Notes: + Automatically manages the reference count of the input and output DDs. + The input DD must have a non-zero reference count. + Args: vec: The input DD. collapse: Whether to collapse the DD. Returns: The measurement outcome. - - Notes: - Automatically manages the reference count of the input and output DDs. - The input DD must have a non-zero reference count. """ @staticmethod def identity() -> MatrixDD: - r"""Create the DD for the identity matrix :math:`I`. + """Create the DD for the identity matrix :math:`I`. + + Notes: + Returns the global one-terminal (identity matrix), which is package-agnostic and safe to use across DDPackage instances. Returns: The DD for the identity matrix. """ - def single_qubit_gate( - self, - matrix: npt.NDArray[np.cdouble], - target: int, - ) -> MatrixDD: + def single_qubit_gate(self, matrix: Annotated[NDArray[np.complex128], {"shape": (2, 2)}], target: int) -> MatrixDD: r"""Create the DD for a single-qubit gate. Args: - matrix: The :math:`2\times 2` matrix representing the single-qubit gate. + matrix: The :math:`2 \times 2` matrix representing the single-qubit gate. target: The target qubit. Returns: @@ -437,12 +453,15 @@ class DDPackage: """ def controlled_single_qubit_gate( - self, matrix: npt.NDArray[np.cdouble], control: Control | int, target: int + self, + matrix: Annotated[NDArray[np.complex128], {"shape": (2, 2)}], + control: mqt.core.ir.operations.Control | int, + target: int, ) -> MatrixDD: r"""Create the DD for a controlled single-qubit gate. Args: - matrix: The :math:`2\times 2` matrix representing the single-qubit gate. + matrix: The :math:`2 \times 2` matrix representing the single-qubit gate. control: The control qubit. target: The target qubit. @@ -452,14 +471,14 @@ class DDPackage: def multi_controlled_single_qubit_gate( self, - matrix: npt.NDArray[np.cdouble], - controls: set[Control | int], + matrix: Annotated[NDArray[np.complex128], {"shape": (2, 2)}], + controls: AbstractSet[mqt.core.ir.operations.Control | int], target: int, ) -> MatrixDD: r"""Create the DD for a multi-controlled single-qubit gate. Args: - matrix: The :math:`2\times 2` matrix representing the single-qubit gate. + matrix: The :math:`2 \times 2` matrix representing the single-qubit gate. controls: The control qubits. target: The target qubit. @@ -468,15 +487,12 @@ class DDPackage: """ def two_qubit_gate( - self, - matrix: npt.NDArray[np.cdouble], - target0: int, - target1: int, + self, matrix: Annotated[NDArray[np.complex128], {"shape": (4, 4)}], target0: int, target1: int ) -> MatrixDD: r"""Create the DD for a two-qubit gate. Args: - matrix: The :math:`4\times 4` matrix representing the two-qubit gate. + matrix: The :math:`4 \times 4` matrix representing the two-qubit gate. target0: The first target qubit. target1: The second target qubit. @@ -486,15 +502,15 @@ class DDPackage: def controlled_two_qubit_gate( self, - matrix: npt.NDArray[np.cdouble], - control: Control | int, + matrix: Annotated[NDArray[np.complex128], {"shape": (4, 4)}], + control: mqt.core.ir.operations.Control | int, target0: int, target1: int, ) -> MatrixDD: r"""Create the DD for a controlled two-qubit gate. Args: - matrix: The :math:`4\times 4` matrix representing the two-qubit gate. + matrix: The :math:`4 \times 4` matrix representing the two-qubit gate. control: The control qubit. target0: The first target qubit. target1: The second target qubit. @@ -505,15 +521,15 @@ class DDPackage: def multi_controlled_two_qubit_gate( self, - matrix: npt.NDArray[np.cdouble], - controls: set[Control | int], + matrix: Annotated[NDArray[np.complex128], {"shape": (4, 4)}], + controls: AbstractSet[mqt.core.ir.operations.Control | int], target0: int, target1: int, ) -> MatrixDD: r"""Create the DD for a multi-controlled two-qubit gate. Args: - matrix: The :math:`4\times 4` matrix representing the two-qubit gate. + matrix: The :math:`4 \times 4` matrix representing the two-qubit gate. controls: The control qubits. target0: The first target qubit. target1: The second target qubit. @@ -522,23 +538,21 @@ class DDPackage: The DD for the multi-controlled two-qubit gate. """ - def from_matrix(self, matrix: npt.NDArray[np.cdouble]) -> MatrixDD: + def from_matrix(self, matrix: Annotated[NDArray[np.complex128], {"shape": (None, None)}]) -> MatrixDD: """Create a DD from a matrix. Args: - matrix: The matrix. - Must be square and have a size that is a power of 2. + matrix: The matrix. Must be square and have a size that is a power of 2. Returns: The DD for the matrix. """ - def from_operation(self, operation: Operation, invert: bool = False) -> MatrixDD: + def from_operation(self, operation: mqt.core.ir.operations.Operation, invert: bool = False) -> MatrixDD: """Create a DD from an operation. Args: - operation: The operation. - Must be unitary. + operation: The operation. Must be unitary. invert: Whether to get the inverse of the operation. Returns: @@ -548,12 +562,12 @@ class DDPackage: def inc_ref_vec(self, vec: VectorDD) -> None: """Increment the reference count of a vector.""" - def dec_ref_vec(self, vec: VectorDD) -> None: - """Decrement the reference count of a vector.""" - def inc_ref_mat(self, mat: MatrixDD) -> None: """Increment the reference count of a matrix.""" + def dec_ref_vec(self, vec: VectorDD) -> None: + """Decrement the reference count of a vector.""" + def dec_ref_mat(self, mat: MatrixDD) -> None: """Decrement the reference count of a matrix.""" @@ -562,9 +576,8 @@ class DDPackage: Args: force: Whether to force garbage collection. - If set to True, garbage collection is performed regardless - of the current memory usage. If set to False, garbage collection - is only performed if the memory usage exceeds a certain threshold. + If set to True, garbage collection is performed regardless of the current memory usage. + If set to False, garbage collection is only performed if the memory usage exceeds a certain threshold. Returns: Whether any nodes were collected during garbage collection. @@ -573,140 +586,133 @@ class DDPackage: def vector_add(self, lhs: VectorDD, rhs: VectorDD) -> VectorDD: """Add two vectors. + Notes: + It is the caller's responsibility to update the reference count of the input and output vectors after the operation. + + Both vectors must have the same number of qubits. + Args: lhs: The left vector. rhs: The right vector. Returns: The sum of the two vectors. - - Notes: - It is the caller's responsibility to update the reference count of the - input and output vectors after the operation. - - Both vectors must have the same number of qubits. """ def matrix_add(self, lhs: MatrixDD, rhs: MatrixDD) -> MatrixDD: """Add two matrices. + Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + + Both matrices must have the same number of qubits. + Args: lhs: The left matrix. rhs: The right matrix. Returns: The sum of the two matrices. - - Notes: - It is the caller's responsibility to update the reference count of the - input and output matrices after the operation. - - Both matrices must have the same number of qubits. """ def conjugate(self, vec: VectorDD) -> VectorDD: """Conjugate a vector. + Notes: + It is the caller's responsibility to update the reference count of the input and output vectors after the operation. + Args: vec: The vector. Returns: The conjugated vector. - - Notes: - It is the caller's responsibility to update the reference count of the - input and output vectors after the operation. """ def conjugate_transpose(self, mat: MatrixDD) -> MatrixDD: """Conjugate transpose a matrix. + Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + Args: mat: The matrix. Returns: The conjugate transposed matrix. - - Notes: - It is the caller's responsibility to update the reference count of the - input and output matrices after the operation. """ def matrix_vector_multiply(self, mat: MatrixDD, vec: VectorDD) -> VectorDD: """Multiply a matrix with a vector. + Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + + The vector must have at least as many qubits as the matrix non-trivially acts on. + Args: mat: The matrix. vec: The vector. Returns: The product of the matrix and the vector. - - Notes: - It is the caller's responsibility to update the reference count of the - input and output matrices after the operation. - - The vector must have at least as many qubits as the matrix non-trivially acts on. """ def matrix_multiply(self, lhs: MatrixDD, rhs: MatrixDD) -> MatrixDD: """Multiply two matrices. + Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + Args: lhs: The left matrix. rhs: The right matrix. Returns: The product of the two matrices. - - Notes: - It is the caller's responsibility to update the reference count of the - input and output matrices after the operation. """ def inner_product(self, lhs: VectorDD, rhs: VectorDD) -> complex: """Compute the inner product of two vectors. + Notes: + Both vectors must have the same number of qubits. + Args: lhs: The left vector. rhs: The right vector. Returns: The inner product of the two vectors. - - Notes: - Both vectors must have the same number of qubits. """ def fidelity(self, lhs: VectorDD, rhs: VectorDD) -> float: """Compute the fidelity of two vectors. + Notes: + Both vectors must have the same number of qubits. + Args: lhs: The left vector. rhs: The right vector. Returns: The fidelity of the two vectors. - - Notes: - Both vectors must have the same number of qubits. """ def expectation_value(self, observable: MatrixDD, state: VectorDD) -> float: r"""Compute the expectation value of an observable. + Notes: + The state must have at least as many qubits as the observable non-trivially acts on. + + The method computes :math:`\langle \psi | O | \psi \rangle` as :math:`\langle \psi | (O | \psi \rangle)`. + Args: observable: The observable. state: The state. Returns: The expectation value of the observable. - - Notes: - The state must have at least as many qubits as the observable non-trivially acts on. - - The method computes :math:`\langle \psi | O | \psi \rangle` as - :math:`\langle \psi | (O | \psi \rangle)`. """ def vector_kronecker( @@ -714,6 +720,9 @@ class DDPackage: ) -> VectorDD: """Compute the Kronecker product of two vectors. + Notes: + It is the caller's responsibility to update the reference count of the input and output vectors after the operation. + Args: top: The top vector. bottom: The bottom vector. @@ -722,10 +731,6 @@ class DDPackage: Returns: The Kronecker product of the two vectors. - - Notes: - It is the caller's responsibility to update the reference count of the - input and output vectors after the operation. """ def matrix_kronecker( @@ -733,6 +738,9 @@ class DDPackage: ) -> MatrixDD: """Compute the Kronecker product of two matrices. + Notes: + It is the caller's responsibility to update the reference count of the input and output matrices after the operation. + Args: top: The top matrix. bottom: The bottom matrix. @@ -741,19 +749,14 @@ class DDPackage: Returns: The Kronecker product of the two matrices. - - Notes: - It is the caller's responsibility to update the reference count of the - input and output matrices after the operation. """ - def partial_trace(self, mat: MatrixDD, eliminate: list[bool]) -> MatrixDD: + def partial_trace(self, mat: MatrixDD, eliminate: Sequence[bool]) -> MatrixDD: """Compute the partial trace of a matrix. Args: mat: The matrix. - eliminate: The qubits to eliminate. - Must be at least as long as the number of qubits of the matrix. + eliminate: The qubits to eliminate. Must be at least as long as the number of qubits of the matrix. Returns: The partial trace of the matrix. @@ -770,247 +773,140 @@ class DDPackage: The trace of the matrix. """ -class VectorDD: - """A class representing a vector decision diagram (DD).""" - - def is_terminal(self) -> bool: - """Check if the DD is a terminal node.""" - - def is_zero_terminal(self) -> bool: - """Check if the DD is a zero terminal node.""" - - def size(self) -> int: - """Get the size of the DD by traversing it once.""" - - def __getitem__(self, index: int) -> complex: - """Get the amplitude of a basis state by index.""" - - def get_amplitude(self, num_qubits: int, decisions: str) -> complex: - """Get the amplitude of a basis state by decisions. - - Args: - num_qubits: The number of qubits. - decisions: The decisions as a string of bits (`0` or `1`), where - `decisions[i]` corresponds to the successor to follow at level `i` of the DD. - Must be at least `num_qubits` long. - - Returns: - The amplitude of the basis state. - """ - - def get_vector(self, threshold: float = 0.0) -> Vector: - """Get the state vector represented by the DD. - - Args: - threshold: The threshold for not including amplitudes in the state vector. - Defaults to 0.0. - - Returns: - The state vector. - """ - - def to_dot( - self, - colored: bool = True, - edge_labels: bool = False, - classic: bool = False, - memory: bool = False, - format_as_polar: bool = True, - ) -> str: - """Convert the DD to a DOT graph that can be plotted via Graphviz. - - Args: - colored: Whether to use colored edge weights - edge_labels: Whether to include edge weights as labels. - classic: Whether to use the classic DD visualization style. - memory: Whether to include memory information. - For debugging purposes only. - format_as_polar: Whether to format the edge weights in polar coordinates. +class BasisStates(enum.Enum): + """Enumeration of basis states.""" - Returns: - The DOT graph. - """ + zero = 0 + r"""The computational basis state :math:`|0\rangle`.""" - def to_svg( - self, - filename: str, - colored: bool = True, - edge_labels: bool = False, - classic: bool = False, - memory: bool = False, - format_as_polar: bool = True, - ) -> None: - """Convert the DD to an SVG file that can be viewed in a browser. + one = 1 + r"""The computational basis state :math:`|1\rangle`.""" - Requires the `dot` command from Graphviz to be installed and available in the PATH. + plus = 2 + r""" + The superposition state :math:`|+\rangle = \frac{1}{\sqrt{2}} (|0\rangle + |1\rangle)`. + """ - Args: - filename: The filename of the SVG file. - Any file extension will be replaced by `.dot` and then `.svg`. - colored: Whether to use colored edge weights. - edge_labels: Whether to include edge weights as labels. - classic: Whether to use the classic DD visualization style. - memory: Whether to include memory information. - For debugging purposes only. - show: Whether to open the SVG file in the default browser. - format_as_polar: Whether to format the edge weights in polar coordinates. - """ + minus = 3 + r""" + The superposition state :math:`|-\rangle = \frac{1}{\sqrt{2}} (|0\rangle - |1\rangle)`. + """ -class MatrixDD: - """A class representing a matrix decision diagram (DD).""" + right = 4 + r""" + The superposition state :math:`|R\rangle = \frac{1}{\sqrt{2}} (|0\rangle - i |1\rangle)`. + """ - def is_terminal(self) -> bool: - """Check if the DD is a terminal node.""" + left = 5 + r""" + The superposition state :math:`|L\rangle = \frac{1}{\sqrt{2}} (|0\rangle + i |1\rangle)`. + """ - def is_zero_terminal(self) -> bool: - """Check if the DD is a zero terminal node.""" +def sample(qc: mqt.core.ir.QuantumComputation, shots: int = 1024, seed: int = 0) -> dict[str, int]: + """Sample from the output distribution of a quantum computation. - def is_identity(self, up_to_global_phase: bool = True) -> bool: - """Check if the DD represents the identity matrix. + This function classically simulates the quantum computation and repeatedly samples from the output distribution. + It supports mid-circuit measurements, resets, and classical control. - Args: - up_to_global_phase: Whether to ignore global phase. + Args: + qc: The quantum computation. + shots: The number of samples to take. + If the quantum computation contains no mid-circuit measurements or resets, the circuit is simulated once and the samples are drawn from the final state. + Otherwise, the circuit is simulated once for each sample. + Defaults to 1024. + seed: The seed for the random number generator. + If set to a specific non-zero value, the simulation is deterministic. + If set to 0, the RNG is randomly seeded. + Defaults to 0. - Returns: - Whether the DD represents the identity matrix. - """ + Returns: + A histogram of the samples. + Each sample is a bitstring representing the measurement outcomes of the qubits in the quantum computation. + The leftmost bit corresponds to the most significant qubit, that is, the qubit with the highest index (big-endian). + If the circuit contains measurements, only the qubits that are actively measured are included in the output distribution. + Otherwise, all qubits in the circuit are measured. + """ - def size(self) -> int: - """Get the size of the DD by traversing it once.""" +def simulate_statevector(qc: mqt.core.ir.QuantumComputation) -> Annotated[NDArray[np.complex128], {"shape": (None,)}]: + """Simulate the quantum computation and return the final state vector. - def get_entry(self, num_qubits: int, row: int, col: int) -> complex: - """Get the entry of the matrix by row and column index.""" + This function classically simulates the quantum computation and returns the state vector of the final state. + It does not support measurements, resets, or classical control. - def get_entry_by_path(self, num_qubits: int, decisions: str) -> complex: - """Get the entry of the matrix by decisions. + Since the state vector is guaranteed to be exponentially large in the number of qubits, this function is only suitable for small quantum computations. + Consider using the :func:`~mqt.core.dd.simulate` or the :func:`~mqt.core.dd.sample` functions, which never explicitly construct the state vector, for larger quantum computations. - Args: - num_qubits: The number of qubits. - decisions: The decisions as a string of `0`, `1`, `2`, or `3`, where - `decisions[i]` corresponds to the successor to follow at level `i` of the DD. - Must be at least `num_qubits` long. + Notes: + This function internally constructs a :class:`~mqt.core.dd.DDPackage`, creates the zero state, and simulates the quantum computation via the :func:`simulate` function. + The state vector is then extracted from the resulting DD via the :meth:`~mqt.core.dd.VectorDD.get_vector` method. - Returns: - The entry of the matrix. - """ + Args: + qc: The quantum computation. Must only contain unitary operations. - def get_matrix(self, num_qubits: int, threshold: float = 0.0) -> Matrix: - """Get the matrix represented by the DD. + Returns: + The state vector of the final state. + """ - Args: - num_qubits: The number of qubits. - threshold: The threshold for not including entries in the matrix. - Defaults to 0.0. +def build_unitary( + qc: mqt.core.ir.QuantumComputation, recursive: bool = False +) -> Annotated[NDArray[np.complex128], {"shape": (None, None)}]: + """Build a unitary matrix representation of a quantum computation. - Returns: - The matrix. - """ + This function builds a matrix representation of the unitary representing the functionality of a quantum computation. + This function does not support measurements, resets, or classical control, as the corresponding operations are non-unitary. - def to_dot( - self, - colored: bool = True, - edge_labels: bool = False, - classic: bool = False, - memory: bool = False, - format_as_polar: bool = True, - ) -> str: - """Convert the DD to a DOT graph that can be plotted via Graphviz. + Since the unitary matrix is guaranteed to be exponentially large in the number of qubits, this function is only suitable for small quantum computations. + Consider using the :func:`~mqt.core.dd.build_functionality` function, which never explicitly constructs the unitary matrix, for larger quantum computations. - Args: - colored: Whether to use colored edge weights - edge_labels: Whether to include edge weights as labels. - classic: Whether to use the classic DD visualization style. - memory: Whether to include memory information. - For debugging purposes only. - format_as_polar: Whether to format the edge weights in polar coordinates. + Notes: + This function internally constructs a :class:`~mqt.core.dd.DDPackage`, creates the identity matrix, and builds the unitary matrix via the :func:`~mqt.core.dd.build_functionality` function. + The unitary matrix is then extracted from the resulting DD via the :meth:`~mqt.core.dd.MatrixDD.get_matrix` method. - Returns: - The DOT graph. - """ + Args: + qc: The quantum computation. Must only contain unitary operations. + recursive: Whether to build the unitary matrix recursively. + If set to True, the unitary matrix is built recursively by pairwise grouping the operations of the quantum computation. + If set to False, the unitary matrix is built by sequentially applying the operations of the quantum computation to the identity matrix. + Defaults to False. - def to_svg( - self, - filename: str, - colored: bool = True, - edge_labels: bool = False, - classic: bool = False, - memory: bool = False, - format_as_polar: bool = True, - ) -> None: - """Convert the DD to an SVG file that can be viewed in a browser. + Returns: + The unitary matrix representing the functionality of the quantum computation. + """ - Requires the `dot` command from Graphviz to be installed and available in the PATH. +def simulate(qc: mqt.core.ir.QuantumComputation, initial_state: VectorDD, dd_package: DDPackage) -> VectorDD: + """Simulate a quantum computation. - Args: - filename: The filename of the SVG file. - Any file extension will be replaced by `.dot` and then `.svg`. - colored: Whether to use colored edge weights. - edge_labels: Whether to include edge weights as labels. - classic: Whether to use the classic DD visualization style. - memory: Whether to include memory information. - For debugging purposes only. - show: Whether to open the SVG file in the default browser. - format_as_polar: Whether to format the edge weights in polar coordinates. - """ + This function classically simulates a quantum computation for a given initial state and returns the final state (represented as a DD). + Compared to the `sample` function, this function does not support measurements, resets, or classical control. + It only supports unitary operations. -class BasisStates(Enum): - """Enumeration of basis states.""" + The simulation is effectively computed by sequentially applying the operations of the quantum computation to the initial state. - zero = ... - r"""The computational basis state :math:`|0\rangle`.""" - one = ... - r"""The computational basis state :math:`|1\rangle`.""" - plus = ... - r"""The superposition state :math:`|+\rangle = \frac{1}{\sqrt{2}}(|0\rangle + |1\rangle)`.""" - minus = ... - r"""The superposition state :math:`|-\rangle = \frac{1}{\sqrt{2}}(|0\rangle - |1\rangle)`.""" - left = ... - r"""The rotational superposition state :math:`|L\rangle = \frac{1}{\sqrt{2}}(|0\rangle + i|1\rangle)`.""" - right = ... - r"""The rotational superposition state :math:`|R\rangle = \frac{1}{\sqrt{2}}(|0\rangle - i|1\rangle)`.""" - -class Vector: - """A class representing a vector of complex numbers. - - Implements the buffer protocol so that the underlying memory can be accessed - and easily converted to a NumPy array without copying. - - Examples: - >>> from mqt.core.dd import DDPackage - ... import numpy as np - ... - ... zero_state = DDPackage(2).zero_state(2) - ... vec = np.array(zero_state.get_vector(), copy=False) - ... print(vec) - [1.+0.j 0.+0.j 0.+0.j 0.+0.j] + Args: + qc: The quantum computation. Must only contain unitary operations. + initial_state: The initial state as a DD. Must have the same number of qubits as the quantum computation. + The reference count of the initial state is decremented during the simulation, so the caller must ensure that the initial state has a non-zero reference count. + dd_package: The DD package. Must be configured with a sufficient number of qubits to accommodate the quantum computation. + Returns: + The final state as a DD. The reference count of the final state is non-zero and must be manually decremented by the caller if it is no longer needed. """ - def __buffer__(self, flags: int, /) -> memoryview: - """Return a buffer object that exposes the underlying memory of the object.""" - - def __release_buffer__(self, buffer: memoryview, /) -> None: - """Release the buffer object that exposes the underlying memory of the object.""" +def build_functionality(qc: mqt.core.ir.QuantumComputation, dd_package: DDPackage, recursive: bool = False) -> MatrixDD: + """Build a functional representation of a quantum computation. -class Matrix: - """A class representing a matrix of complex numbers. + This function builds a matrix DD representation of the unitary representing the functionality of a quantum computation. + This function does not support measurements, resets, or classical control, as the corresponding operations are non-unitary. - Implements the buffer protocol so that the underlying memory can be accessed - and easily converted to a NumPy array without copying. + Args: + qc: The quantum computation. + Must only contain unitary operations. + dd_package: The DD package. Must be configured with a sufficient number of qubits to accommodate the quantum computation. + recursive: Whether to build the functionality matrix recursively. + If set to True, the functionality matrix is built recursively by pairwise grouping the operations of the quantum computation. + If set to False, the functionality matrix is built by sequentially applying the operations of the quantum computation to the identity matrix. + Defaults to False. - Examples: - >>> from mqt.core.dd import DDPackage - ... import numpy as np - ... - ... identity = DDPackage(1).identity() - ... mat = np.array(identity.get_matrix(1), copy=False) - ... print(mat) - [[1.+0.j 0.+0.j] - [0.+0.j 1.+0.j]] + Returns: + The functionality as a DD. The reference count of the result is non-zero and must be manually decremented by the caller if it is no longer needed. """ - - def __buffer__(self, flags: int, /) -> memoryview: - """Return a buffer object that exposes the underlying memory of the object.""" - - def __release_buffer__(self, buffer: memoryview, /) -> None: - """Release the buffer object that exposes the underlying memory of the object.""" diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi index 4b23b8909a..60d00b5514 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/fomac.pyi @@ -6,45 +6,14 @@ # # Licensed under the MIT License -import sys -from collections.abc import Iterable -from enum import Enum - -__all__ = [ - "Device", - "Job", - "ProgramFormat", - "Session", -] - -# add_dynamic_device_library is only available on non-Windows platforms -if sys.platform != "win32": - __all__ += ["add_dynamic_device_library"] - -class ProgramFormat(Enum): - """Enumeration of program formats.""" - - QASM2 = ... - QASM3 = ... - QIR_BASE_STRING = ... - QIR_BASE_MODULE = ... - QIR_ADAPTIVE_STRING = ... - QIR_ADAPTIVE_MODULE = ... - CALIBRATION = ... - QPY = ... - IQM_JSON = ... - CUSTOM1 = ... - CUSTOM2 = ... - CUSTOM3 = ... - CUSTOM4 = ... - CUSTOM5 = ... +import enum +from collections.abc import Sequence class Session: """A FoMaC session for managing QDMI devices. Allows creating isolated sessions with independent authentication settings. - All authentication parameters are optional and can be provided as keyword - arguments to the constructor. + All authentication parameters are optional and can be provided as keyword arguments to the constructor. """ def __init__( @@ -106,25 +75,15 @@ class Session: """Get available devices from this session. Returns: - List of available devices + List of available devices. """ class Job: """A job represents a submitted quantum program execution.""" - class Status(Enum): - """Enumeration of job status.""" - - CREATED = ... - SUBMITTED = ... - QUEUED = ... - RUNNING = ... - DONE = ... - CANCELED = ... - FAILED = ... - def check(self) -> Job.Status: """Returns the current status of the job.""" + def wait(self, timeout: int = 0) -> bool: """Waits for the job to complete. @@ -134,208 +93,311 @@ class Job: Returns: True if the job completed within the timeout, False otherwise. """ + def cancel(self) -> None: """Cancels the job.""" + def get_shots(self) -> list[str]: """Returns the raw shot results from the job.""" + def get_counts(self) -> dict[str, int]: """Returns the measurement counts from the job.""" + def get_dense_statevector(self) -> list[complex]: """Returns the dense statevector from the job (typically only available from simulator devices).""" - def get_sparse_statevector(self) -> dict[str, complex]: - """Returns the sparse statevector from the job (typically only available from simulator devices).""" + def get_dense_probabilities(self) -> list[float]: """Returns the dense probabilities from the job (typically only available from simulator devices).""" + + def get_sparse_statevector(self) -> dict[str, complex]: + """Returns the sparse statevector from the job (typically only available from simulator devices).""" + def get_sparse_probabilities(self) -> dict[str, float]: """Returns the sparse probabilities from the job (typically only available from simulator devices).""" - def __eq__(self, other: object) -> bool: - """Checks if two jobs are equal.""" - def __ne__(self, other: object) -> bool: - """Checks if two jobs are not equal.""" + @property def id(self) -> str: """Returns the job ID.""" + @property def program_format(self) -> ProgramFormat: """Returns the program format used for the job.""" + @property def program(self) -> str: """Returns the quantum program submitted for the job.""" + @property def num_shots(self) -> int: """Returns the number of shots for the job.""" + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + + class Status(enum.Enum): + """Enumeration of job status.""" + + CREATED = 0 + + SUBMITTED = 1 + + QUEUED = 2 + + RUNNING = 3 + + DONE = 4 + + CANCELED = 5 + + FAILED = 6 + +class ProgramFormat(enum.Enum): + """Enumeration of program formats.""" + + QASM2 = 0 + + QASM3 = 1 + + QIR_BASE_STRING = 2 + + QIR_BASE_MODULE = 3 + + QIR_ADAPTIVE_STRING = 4 + + QIR_ADAPTIVE_MODULE = 5 + + CALIBRATION = 6 + + QPY = 7 + + IQM_JSON = 8 + + CUSTOM1 = 999999995 + + CUSTOM2 = 999999996 + + CUSTOM3 = 999999997 + + CUSTOM4 = 999999998 + + CUSTOM5 = 999999999 + class Device: """A device represents a quantum device with its properties and capabilities.""" - class Status(Enum): - offline = ... - idle = ... - busy = ... - error = ... - maintenance = ... - calibration = ... + + class Status(enum.Enum): + """Enumeration of device status.""" + + OFFLINE = 0 + + IDLE = 1 + + BUSY = 2 + + ERROR = 3 + + MAINTENANCE = 4 + + CALIBRATION = 5 + + def name(self) -> str: + """Returns the name of the device.""" + + def version(self) -> str: + """Returns the version of the device.""" + + def status(self) -> Device.Status: + """Returns the current status of the device.""" + + def library_version(self) -> str: + """Returns the version of the library used to define the device.""" + + def qubits_num(self) -> int: + """Returns the number of qubits available on the device.""" + + def sites(self) -> list[Device.Site]: + """Returns the list of all sites (zone and regular sites) available on the device.""" + + def regular_sites(self) -> list[Device.Site]: + """Returns the list of regular sites (without zone sites) available on the device.""" + + def zones(self) -> list[Device.Site]: + """Returns the list of zone sites (without regular sites) available on the device.""" + + def operations(self) -> list[Device.Operation]: + """Returns the list of operations supported by the device.""" + + def coupling_map(self) -> list[tuple[Device.Site, Device.Site]] | None: + """Returns the coupling map of the device as a list of site pairs.""" + + def needs_calibration(self) -> int | None: + """Returns whether the device needs calibration.""" + + def length_unit(self) -> str | None: + """Returns the unit of length used by the device.""" + + def length_scale_factor(self) -> float | None: + """Returns the scale factor for length used by the device.""" + + def duration_unit(self) -> str | None: + """Returns the unit of duration used by the device.""" + + def duration_scale_factor(self) -> float | None: + """Returns the scale factor for duration used by the device.""" + + def min_atom_distance(self) -> int | None: + """Returns the minimum atom distance on the device.""" + + def supported_program_formats(self) -> list[ProgramFormat]: + """Returns the list of program formats supported by the device.""" + + def submit_job(self, program: str, program_format: ProgramFormat, num_shots: int) -> Job: + """Submits a job to the device.""" + + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... class Site: """A site represents a potential qubit location on a quantum device.""" + def index(self) -> int: """Returns the index of the site.""" + def t1(self) -> int | None: """Returns the T1 coherence time of the site.""" + def t2(self) -> int | None: """Returns the T2 coherence time of the site.""" + def name(self) -> str | None: """Returns the name of the site.""" + def x_coordinate(self) -> int | None: """Returns the x coordinate of the site.""" + def y_coordinate(self) -> int | None: """Returns the y coordinate of the site.""" + def z_coordinate(self) -> int | None: """Returns the z coordinate of the site.""" + def is_zone(self) -> bool: """Returns whether the site is a zone.""" + def x_extent(self) -> int | None: """Returns the x extent of the site.""" + def y_extent(self) -> int | None: """Returns the y extent of the site.""" + def z_extent(self) -> int | None: """Returns the z extent of the site.""" + def module_index(self) -> int | None: """Returns the index of the module the site belongs to.""" + def submodule_index(self) -> int | None: """Returns the index of the submodule the site belongs to.""" - def __eq__(self, other: object) -> bool: - """Checks if two sites are equal.""" - def __ne__(self, other: object) -> bool: - """Checks if two sites are not equal.""" + + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... class Operation: """An operation represents a quantum operation that can be performed on a quantum device.""" - def name(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> str: + + def name(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> str: """Returns the name of the operation.""" - def qubits_num(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> int | None: + + def qubits_num(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: """Returns the number of qubits the operation acts on.""" - def parameters_num(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> int: + + def parameters_num(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int: """Returns the number of parameters the operation has.""" - def duration(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> int | None: + + def duration(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: """Returns the duration of the operation.""" - def fidelity(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> float | None: + + def fidelity(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> float | None: """Returns the fidelity of the operation.""" - def interaction_radius(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> int | None: + + def interaction_radius(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: """Returns the interaction radius of the operation.""" - def blocking_radius(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> int | None: + + def blocking_radius(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: """Returns the blocking radius of the operation.""" - def idling_fidelity(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> float | None: + + def idling_fidelity(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> float | None: """Returns the idling fidelity of the operation.""" + def is_zoned(self) -> bool: """Returns whether the operation is zoned.""" + def sites(self) -> list[Device.Site] | None: """Returns the list of sites the operation can be performed on.""" + def site_pairs(self) -> list[tuple[Device.Site, Device.Site]] | None: """Returns the list of site pairs the local 2-qubit operation can be performed on.""" - def mean_shuttling_speed(self, sites: Iterable[Device.Site] = ..., params: Iterable[float] = ...) -> int | None: - """Returns the mean shuttling speed of the operation.""" - def __eq__(self, other: object) -> bool: - """Checks if two operations are equal.""" - def __ne__(self, other: object) -> bool: - """Checks if two operations are not equal.""" - def name(self) -> str: - """Returns the name of the device.""" - def version(self) -> str: - """Returns the version of the device.""" - def status(self) -> Device.Status: - """Returns the current status of the device.""" - def library_version(self) -> str: - """Returns the version of the library used to define the device.""" - def qubits_num(self) -> int: - """Returns the number of qubits available on the device.""" - def sites(self) -> list[Site]: - """Returns the list of all sites (zone and regular sites) available on the device.""" - def regular_sites(self) -> list[Site]: - """Returns the list of regular sites (without zone sites) available on the device.""" - def zones(self) -> list[Site]: - """Returns the list of zone sites (without regular sites) available on the device.""" - def operations(self) -> list[Operation]: - """Returns the list of operations supported by the device.""" - def coupling_map(self) -> list[tuple[Site, Site]] | None: - """Returns the coupling map of the device as a list of site pairs.""" - def needs_calibration(self) -> int | None: - """Returns whether the device needs calibration.""" - def length_unit(self) -> str | None: - """Returns the unit of length used by the device.""" - def length_scale_factor(self) -> float | None: - """Returns the scale factor for length used by the device.""" - def duration_unit(self) -> str | None: - """Returns the unit of duration used by the device.""" - def duration_scale_factor(self) -> float | None: - """Returns the scale factor for duration used by the device.""" - def min_atom_distance(self) -> int | None: - """Returns the minimum atom distance on the device.""" - def supported_program_formats(self) -> list[ProgramFormat]: - """Returns the list of program formats supported by the device.""" - def submit_job(self, program: str, program_format: ProgramFormat, num_shots: int) -> Job: - """Submits a job to the device.""" - def __eq__(self, other: object) -> bool: - """Checks if two devices are equal.""" - def __ne__(self, other: object) -> bool: - """Checks if two devices are not equal.""" - -# Dynamic library loading is only available on non-Windows platforms -if sys.platform != "win32": - def add_dynamic_device_library( - library_path: str, - prefix: str, - *, - base_url: str | None = None, - token: str | None = None, - auth_file: str | None = None, - auth_url: str | None = None, - username: str | None = None, - password: str | None = None, - custom1: str | None = None, - custom2: str | None = None, - custom3: str | None = None, - custom4: str | None = None, - custom5: str | None = None, - ) -> Device: - """Load a dynamic device library into the QDMI driver. - - This function loads a shared library (.so, .dll, or .dylib) that implements - a QDMI device interface and makes it available for use in sessions. - - Note: This function is only available on non-Windows platforms. - - Args: - library_path: Path to the shared library file to load. - prefix: Function prefix used by the library (e.g., "MY_DEVICE"). - base_url: Optional base URL for the device API endpoint. - token: Optional authentication token. - auth_file: Optional path to authentication file. - auth_url: Optional authentication server URL. - username: Optional username for authentication. - password: Optional password for authentication. - custom1: Optional custom configuration parameter 1. - custom2: Optional custom configuration parameter 2. - custom3: Optional custom configuration parameter 3. - custom4: Optional custom configuration parameter 4. - custom5: Optional custom configuration parameter 5. - - Returns: - Device: The newly loaded device that can be used to create backends. - - Raises: - RuntimeError: If library loading fails or configuration is invalid. - - Examples: - Load a device library with configuration: - - >>> import mqt.core.fomac as fomac - >>> device = fomac.add_dynamic_device_library( - ... "/path/to/libmy_device.so", "MY_DEVICE", base_url="http://localhost:8080", custom1="API_V2" - ... ) - - Now the device can be used directly: + def mean_shuttling_speed(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: + """Returns the mean shuttling speed of the operation.""" - >>> from mqt.core.plugins.qiskit import QDMIBackend - >>> backend = QDMIBackend(device=device) - """ + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + +def add_dynamic_device_library( + library_path: str, + prefix: str, + *, + base_url: str | None = None, + token: str | None = None, + auth_file: str | None = None, + auth_url: str | None = None, + username: str | None = None, + password: str | None = None, + custom1: str | None = None, + custom2: str | None = None, + custom3: str | None = None, + custom4: str | None = None, + custom5: str | None = None, +) -> Device: + """Load a dynamic device library into the QDMI driver. + + This function loads a shared library (.so, .dll, or .dylib) that implements a QDMI device interface and makes it available for use in sessions. + + Note: + This function is only available on non-Windows platforms. + + Args: + library_path: Path to the shared library file to load. + prefix: Function prefix used by the library (e.g., "MY_DEVICE"). + base_url: Optional base URL for the device API endpoint. + token: Optional authentication token. + auth_file: Optional path to authentication file. + auth_url: Optional authentication server URL. + username: Optional username for authentication. + password: Optional password for authentication. + custom1: Optional custom configuration parameter 1. + custom2: Optional custom configuration parameter 2. + custom3: Optional custom configuration parameter 3. + custom4: Optional custom configuration parameter 4. + custom5: Optional custom configuration parameter 5. + + Returns: + Device: The newly loaded device that can be used to create backends. + + Raises: + RuntimeError: If library loading fails or configuration is invalid. + + Examples: + Load a device library with configuration: + + >>> import mqt.core.fomac as fomac + >>> device = fomac.add_dynamic_device_library( + ... "/path/to/libmy_device.so", "MY_DEVICE", base_url="http://localhost:8080", custom1="API_V2" + ... ) + + Now the device can be used directly: + + >>> from mqt.core.plugins.qiskit import QDMIBackend + >>> backend = QDMIBackend(device=device) + """ diff --git a/python/mqt/core/ir/__init__.pyi b/python/mqt/core/ir/__init__.pyi index 7f46fa37f1..a27e6b8052 100644 --- a/python/mqt/core/ir/__init__.pyi +++ b/python/mqt/core/ir/__init__.pyi @@ -6,105 +6,90 @@ # # Licensed under the MIT License -"""MQT Core IR - The MQT Core Intermediate Representation (IR) module.""" - +import os from collections.abc import ItemsView, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence -from os import PathLike +from collections.abc import Set as AbstractSet from typing import overload -from .operations import ComparisonKind, Control, Operation, OpType -from .registers import ClassicalRegister, QuantumRegister -from .symbolic import Expression, Variable - -__all__ = [ - "Permutation", - "QuantumComputation", -] +from . import operations as operations +from . import registers as registers +from . import symbolic as symbolic class Permutation(MutableMapping[int, int]): """A class to represent a permutation of the qubits in a quantum circuit. Args: permutation: The permutation to initialize the object with. - """ - def __init__(self, permutation: dict[int, int] | None = None) -> None: - """Initialize the permutation.""" + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, permutation: dict[int, int]) -> None: + """Create a permutation from a dictionary.""" - def __getitem__(self, idx: int) -> int: - """Get the value of the permutation at the given index. + @overload + def apply(self, controls: AbstractSet[operations.Control]) -> set[operations.Control]: + """Apply the permutation to a set of controls. Args: - idx: The index to get the value of the permutation at. + controls: The set of controls to apply the permutation to. Returns: - The value of the permutation at the given index. + The set of controls with the permutation applied. """ - def __setitem__(self, idx: int, val: int) -> None: - """Set the value of the permutation at the given index. + @overload + def apply(self, targets: Sequence[int]) -> list[int]: + """Apply the permutation to a list of targets. Args: - idx: The index to set the value of the permutation at. - val: The value to set the permutation at the given index to. - """ - - def __delitem__(self, key: int) -> None: - """Delete the value of the permutation at the given index. + targets: The list of targets to apply the permutation to. - Args: - key: The index to delete the value of the permutation at. + Returns: + The list of targets with the permutation applied. """ - def __iter__(self) -> Iterator[int]: - """Return an iterator over the indices of the permutation.""" - - def items(self) -> ItemsView[int, int]: - """Return an iterable over the items of the permutation.""" - - def __len__(self) -> int: - """Return the number of indices in the permutation.""" - - def __eq__(self, other: object) -> bool: - """Check if the permutation is equal to another permutation.""" - - def __ne__(self, other: object) -> bool: - """Check if the permutation is not equal to another permutation.""" - - def __hash__(self) -> int: - """Return the hash of the permutation.""" - def clear(self) -> None: """Clear the permutation of all indices and values.""" - @overload - def apply(self, controls: set[Control]) -> set[Control]: - """Apply the permutation to a set of controls. + def __getitem__(self, index: int) -> int: + """Get the value of the permutation at the given index. Args: - controls: The set of controls to apply the permutation to. + index: The index to get the value of the permutation at. Returns: - The set of controls with the permutation applied. + The value of the permutation at the given index. """ - @overload - def apply(self, targets: list[int]) -> list[int]: - """Apply the permutation to a list of targets. + def __setitem__(self, index: int, value: int) -> None: + """Set the value of the permutation at the given index. Args: - targets: The list of targets to apply the permutation to. + index: The index to set the value of the permutation at. + value: The value to set the permutation at the given index to. + """ - Returns: - The list of targets with the permutation applied. + def __delitem__(self, index: int) -> None: + """Delete the value of the permutation at the given index. + + Args: + index: The index to delete the value of the permutation at. """ -class QuantumComputation(MutableSequence[Operation]): + def __len__(self) -> int: + """Return the number of indices in the permutation.""" + + def __iter__(self) -> Iterator[int]: ... + def items(self) -> ItemsView[int, int]: ... + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + +class QuantumComputation(MutableSequence[operations.Operation]): """The main class for representing quantum computations within the MQT. - Acts as mutable sequence of :class:`~mqt.core.ir.operations.Operation` objects, - which represent the individual operations in the quantum computation. + Acts as mutable sequence of :class:`~mqt.core.ir.operations.Operation` objects, which represent the individual operations in the quantum computation. Args: nq: The number of qubits in the quantum computation. @@ -112,9 +97,6 @@ class QuantumComputation(MutableSequence[Operation]): seed: The seed to use for the internal random number generator. """ - # -------------------------------------------------------------------------- - # Constructors - # -------------------------------------------------------------------------- def __init__(self, nq: int = 0, nc: int = 0, seed: int = 0) -> None: ... @staticmethod def from_qasm_str(qasm: str) -> QuantumComputation: @@ -126,6 +108,7 @@ class QuantumComputation(MutableSequence[Operation]): Returns: The QuantumComputation object created from the OpenQASM string. """ + @staticmethod def from_qasm(filename: str) -> QuantumComputation: """Create a QuantumComputation object from an OpenQASM file. @@ -137,19 +120,12 @@ class QuantumComputation(MutableSequence[Operation]): The QuantumComputation object created from the OpenQASM file. """ - # -------------------------------------------------------------------------- - # General Properties - # -------------------------------------------------------------------------- - - name: str - """ - The name of the quantum computation. - """ - global_phase: float - """ - The global phase of the quantum computation. - """ + @property + def name(self) -> str: + """The name of the quantum computation.""" + @name.setter + def name(self, arg: str, /) -> None: ... @property def num_qubits(self) -> int: """The total number of qubits in the quantum computation.""" @@ -172,16 +148,16 @@ class QuantumComputation(MutableSequence[Operation]): @property def num_measured_qubits(self) -> int: - """The number of qubits that are measured in the quantum computation. + r"""The number of qubits that are measured in the quantum computation. - Computed as :math:`|qubits| - |garbage|`. + Computed as :math:`| \text{qubits} | - | \text{garbage} |`. """ @property def num_data_qubits(self) -> int: - """The number of data qubits in the quantum computation. + r"""The number of data qubits in the quantum computation. - Computed as :math:`|qubits| - |ancilla|`. + Computed as :math:`| \text{qubits} | - | \text{ancilla} |`. """ @property @@ -204,31 +180,28 @@ class QuantumComputation(MutableSequence[Operation]): def depth(self) -> int: """Return the depth of the quantum computation.""" + @property + def global_phase(self) -> float: + """The global phase of the quantum computation.""" + + @global_phase.setter + def global_phase(self, arg: float, /) -> None: ... def invert(self) -> None: """Invert the quantum computation in-place by inverting each operation and reversing the order of operations.""" - def to_operation(self) -> Operation: + def to_operation(self) -> operations.Operation: """Convert the quantum computation to a single operation. - This gives ownership of the operations to the resulting operation, - so the quantum computation will be empty after this operation. + This gives ownership of the operations to the resulting operation, so the quantum computation will be empty after this operation. - When the quantum computation contains more than one operation, - the resulting operation is a :class:`~mqt.core.ir.operations.CompoundOperation`. + When the quantum computation contains more than one operation, the resulting operation is a :class:`~mqt.core.ir.operations.CompoundOperation`. Returns: The operation representing the quantum computation. """ - # -------------------------------------------------------------------------- - # Mutable Sequence Interface - # -------------------------------------------------------------------------- - - def __len__(self) -> int: - """Return the number of operations in the quantum computation.""" - @overload - def __getitem__(self, index: int) -> Operation: + def __getitem__(self, index: int) -> operations.Operation: """Get the operation at the given index. Note: @@ -242,7 +215,7 @@ class QuantumComputation(MutableSequence[Operation]): """ @overload - def __getitem__(self, index: slice) -> list[Operation]: + def __getitem__(self, index: slice) -> list[operations.Operation]: """Get a slice of operations from the quantum computation. Note: @@ -256,7 +229,7 @@ class QuantumComputation(MutableSequence[Operation]): """ @overload - def __setitem__(self, index: int, value: Operation) -> None: + def __setitem__(self, index: int, value: operations.Operation) -> None: """Set the operation at the given index. Args: @@ -265,7 +238,7 @@ class QuantumComputation(MutableSequence[Operation]): """ @overload - def __setitem__(self, index: slice, value: Iterable[Operation]) -> None: + def __setitem__(self, index: slice, value: Iterable[operations.Operation]) -> None: """Set the operations in the given slice. Args: @@ -289,7 +262,10 @@ class QuantumComputation(MutableSequence[Operation]): index: The slice of operations to delete. """ - def insert(self, index: int, value: Operation) -> None: + def __len__(self) -> int: + """Return the number of operations in the quantum computation.""" + + def insert(self, index: int, value: operations.Operation) -> None: """Insert an operation at the given index. Args: @@ -297,7 +273,7 @@ class QuantumComputation(MutableSequence[Operation]): value: The operation to insert. """ - def append(self, value: Operation) -> None: + def append(self, value: operations.Operation) -> None: """Append an operation to the end of the quantum computation. Args: @@ -310,22 +286,18 @@ class QuantumComputation(MutableSequence[Operation]): def clear(self) -> None: """Clear the quantum computation of all operations.""" - # -------------------------------------------------------------------------- - # (Qu)Bit Registers - # -------------------------------------------------------------------------- - - def add_ancillary_register(self, n: int, name: str = "anc") -> QuantumRegister: - """Add an ancillary register to the quantum computation. + def add_qubit_register(self, n: int, name: str = "q") -> registers.QuantumRegister: + """Add a qubit register to the quantum computation. Args: - n: The number of qubits in the ancillary register. - name: The name of the ancillary register. + n: The number of qubits in the qubit register. + name: The name of the qubit register. Returns: - The ancillary register added to the quantum computation. + The qubit register added to the quantum computation. """ - def add_classical_register(self, n: int, name: str = "c") -> ClassicalRegister: + def add_classical_register(self, n: int, name: str = "c") -> registers.ClassicalRegister: """Add a classical register to the quantum computation. Args: @@ -336,18 +308,18 @@ class QuantumComputation(MutableSequence[Operation]): The classical register added to the quantum computation. """ - def add_qubit_register(self, n: int, name: str = "q") -> QuantumRegister: - """Add a qubit register to the quantum computation. + def add_ancillary_register(self, n: int, name: str = "anc") -> registers.QuantumRegister: + """Add an ancillary register to the quantum computation. Args: - n: The number of qubits in the qubit register. - name: The name of the qubit register. + n: The number of qubits in the ancillary register. + name: The name of the ancillary register. Returns: - The qubit register added to the quantum computation. + The ancillary register added to the quantum computation. """ - def unify_quantum_registers(self, name: str = "q") -> QuantumRegister: + def unify_quantum_registers(self, name: str = "q") -> registers.QuantumRegister: """Unify all quantum registers in the quantum computation. Args: @@ -358,81 +330,59 @@ class QuantumComputation(MutableSequence[Operation]): """ @property - def qregs(self) -> dict[str, QuantumRegister]: + def qregs(self) -> dict[str, registers.QuantumRegister]: """The quantum registers in the quantum computation.""" @property - def cregs(self) -> dict[str, ClassicalRegister]: + def cregs(self) -> dict[str, registers.ClassicalRegister]: """The classical registers in the quantum computation.""" @property - def ancregs(self) -> dict[str, QuantumRegister]: + def ancregs(self) -> dict[str, registers.QuantumRegister]: """The ancillary registers in the quantum computation.""" - # -------------------------------------------------------------------------- - # Initial Layout and Output Permutation - # -------------------------------------------------------------------------- + @property + def initial_layout(self) -> Permutation: + """The initial layout of the qubits in the quantum computation. - initial_layout: Permutation - """ - The initial layout of the qubits in the quantum computation. + This is a permutation of the qubits in the quantum computation. + It is mainly used to track the mapping of circuit qubits to device qubits during quantum circuit compilation. + The keys are the device qubits (in which a compiled circuit is expressed in), and the values are the circuit qubits (in which the original quantum circuit is expressed in). - This is a permutation of the qubits in the quantum computation. It is mainly - used to track the mapping of circuit qubits to device qubits during quantum - circuit compilation. The keys are the device qubits (in which a compiled circuit - is expressed in), and the values are the circuit qubits (in which the original - quantum circuit is expressed in). + Any operations in the quantum circuit are expected to be expressed in terms of the keys of the initial layout. - Any operations in the quantum circuit are expected to be expressed in terms - of the keys of the initial layout. + Examples: + - If no initial layout is explicitly specified (which is the default), the initial layout is assumed to be the identity permutation. + - Assume a three-qubit circuit has been compiled to a four qubit device and circuit qubit 0 is mapped to device qubit 1, circuit qubit 1 is mapped to device qubit 2, and circuit qubit 2 is mapped to device qubit 3. + Then the initial layout is {1: 0, 2: 1, 3: 2}. + """ - Examples: - - If no initial layout is explicitly specified (which is the default), - the initial layout is assumed to be the identity permutation. - - Assume a three-qubit circuit has been compiled to a four qubit device - and circuit qubit 0 is mapped to device qubit 1, circuit qubit 1 is - mapped to device qubit 2, and circuit qubit 2 is mapped to device qubit 3. - Then the initial layout is {1: 0, 2: 1, 3: 2}. + @initial_layout.setter + def initial_layout(self, arg: Permutation, /) -> None: ... + @property + def output_permutation(self) -> Permutation: + """The output permutation of the qubits in the quantum computation. - """ - output_permutation: Permutation - """ - The output permutation of the qubits in the quantum computation. - - This is a permutation of the qubits in the quantum computation. It is mainly - used to track where individual qubits end up at the end of the quantum computation, - for example after a circuit has been compiled to a specific device and SWAP - gates have been inserted, which permute the qubits. The keys are the qubits - in the circuit and the values are the actual qubits being measured. - - Similar to the initial layout, the keys in the output permutation are the - qubits actually present in the circuit and the values are the qubits in the - "original" circuit. - - Examples: - - If no output permutation is explicitly specified and the circuit does - not contain measurements at the end, the output permutation is assumed - to be the identity permutation. - - If the circuit contains measurements at the end, these measurements - are used to infer the output permutation. Assume a three-qubit circuit - has been compiled to a four qubit device and, at the end of the circuit, - circuit qubit 0 is measured into classical bit 2, circuit qubit 1 is - measured into classical bit 1, and circuit qubit 3 is measured into - classical bit 0. Then the output permutation is {0: 2, 1: 1, 3: 0}. - """ + This is a permutation of the qubits in the quantum computation. + It is mainly used to track where individual qubits end up at the end of the quantum computation, for example after a circuit has been compiled to a specific device and SWAP gates have been inserted, which permute the qubits. + Similar to the initial layout, the keys are the qubits in the circuit and the values are the qubits in the "original" circuit. + + Examples: + - If no output permutation is explicitly specified and the circuit does not contain measurements at the end, the output permutation is assumed to be the identity permutation. + - If the circuit contains measurements at the end, these measurements are used to infer the output permutation. + Assume a three-qubit circuit has been compiled to a four qubit device and, at the end of the circuit, circuit qubit 0 is measured into classical bit 2, circuit qubit 1 is measured into classical bit 1, and circuit qubit 3 is measured into classical bit 0. + Then the output permutation is {0: 2, 1: 1, 3: 0}. + """ + @output_permutation.setter + def output_permutation(self, arg: Permutation, /) -> None: ... def initialize_io_mapping(self) -> None: """Initialize the I/O mapping of the quantum computation. - If no initial layout is explicitly specified, the initial layout is assumed - to be the identity permutation. If the circuit contains measurements at the - end, these measurements are used to infer the output permutation. + If no initial layout is explicitly specified, the initial layout is assumed to be the identity permutation. + If the circuit contains measurements at the end, these measurements are used to infer the output permutation. """ - # -------------------------------------------------------------------------- - # Ancilla and Garbage Handling - # -------------------------------------------------------------------------- - @property def ancillary(self) -> list[bool]: """A list of booleans indicating whether each qubit is ancillary.""" @@ -491,22 +441,18 @@ class QuantumComputation(MutableSequence[Operation]): True if the circuit qubit is garbage, False otherwise. """ - # -------------------------------------------------------------------------- - # Symbolic Circuit Handling - # -------------------------------------------------------------------------- - @property - def variables(self) -> set[Variable]: + def variables(self) -> set[symbolic.Variable]: """The set of variables in the quantum computation.""" - def add_variable(self, var: Expression | float) -> None: + def add_variable(self, var: symbolic.Expression | float) -> None: """Add a variable to the quantum computation. Args: var: The variable to add. """ - def add_variables(self, vars_: Sequence[Expression | float]) -> None: + def add_variables(self, vars_: Sequence[symbolic.Expression | float]) -> None: """Add multiple variables to the quantum computation. Args: @@ -520,7 +466,7 @@ class QuantumComputation(MutableSequence[Operation]): True if the quantum computation is free of variables, False otherwise. """ - def instantiate(self, assignment: Mapping[Variable, float]) -> QuantumComputation: + def instantiate(self, assignment: Mapping[symbolic.Variable, float]) -> QuantumComputation: """Instantiate the quantum computation with the given variable assignment. Args: @@ -530,33 +476,27 @@ class QuantumComputation(MutableSequence[Operation]): The instantiated quantum computation. """ - def instantiate_inplace(self, assignment: Mapping[Variable, float]) -> None: + def instantiate_inplace(self, assignment: Mapping[symbolic.Variable, float]) -> None: """Instantiate the quantum computation with the given variable assignment in-place. Args: assignment: The variable assignment to instantiate the quantum computation with. """ - # -------------------------------------------------------------------------- - # Output Handling - # -------------------------------------------------------------------------- - def qasm2_str(self) -> str: """Return the OpenQASM2 representation of the quantum computation as a string. Note: - This uses some custom extensions to OpenQASM 2.0 that allow for easier - definition of multi-controlled gates. These extensions might not be - supported by all OpenQASM 2.0 parsers. Consider using the :meth:`qasm3_str` - method instead, which uses OpenQASM 3.0 that natively supports - multi-controlled gates. The export also assumes the bigger, non-standard - `qelib1.inc` from Qiskit is available. + This uses some custom extensions to OpenQASM 2.0 that allow for easier definition of multi-controlled gates. + These extensions might not be supported by all OpenQASM 2.0 parsers. + Consider using the :meth:`qasm3_str` method instead, which uses OpenQASM 3.0 that natively supports multi-controlled gates. + The export also assumes the bigger, non-standard `qelib1.inc` from Qiskit is available. Returns: The OpenQASM2 representation of the quantum computation as a string. """ - def qasm2(self, filename: PathLike[str] | str) -> None: + def qasm2(self, filename: os.PathLike[str] | str) -> None: """Write the OpenQASM2 representation of the quantum computation to a file. See Also: @@ -573,7 +513,7 @@ class QuantumComputation(MutableSequence[Operation]): The OpenQASM3 representation of the quantum computation as a string. """ - def qasm3(self, filename: PathLike[str] | str) -> None: + def qasm3(self, filename: os.PathLike[str] | str) -> None: """Write the OpenQASM3 representation of the quantum computation to a file. See Also: @@ -583,10 +523,6 @@ class QuantumComputation(MutableSequence[Operation]): filename: The filename of the file to write the OpenQASM3 representation to. """ - # -------------------------------------------------------------------------- - # Operations - # -------------------------------------------------------------------------- - def i(self, q: int) -> None: r"""Apply an identity operation. @@ -597,26 +533,26 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def ci(self, control: Control | int, target: int) -> None: + def ci(self, control: operations.Control | int, target: int) -> None: """Apply a controlled identity operation. + See Also: + :meth:`i` + Args: control: The control qubit target: The target qubit - - See Also: - :meth:`i` """ - def mci(self, controls: set[Control | int], target: int) -> None: + def mci(self, controls: AbstractSet[operations.Control | int], target: int) -> None: """Apply a multi-controlled identity operation. + See Also: + :meth:`i` + Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`i` """ def x(self, q: int) -> None: @@ -629,26 +565,26 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def cx(self, control: Control | int, target: int) -> None: - """Apply a controlled Pauli-X (CNOT / CX) gate. + def cx(self, control: operations.Control | int, target: int) -> None: + """Apply a controlled Pauli-X (i.e., CNOT or CX) gate. + + See Also: + :meth:`x` Args: control: The control qubit target: The target qubit + """ + + def mcx(self, controls: AbstractSet[operations.Control | int], target: int) -> None: + """Apply a multi-controlled Pauli-X (i.e., Toffoli or MCX) gate. See Also: :meth:`x` - """ - - def mcx(self, controls: set[Control | int], target: int) -> None: - """Apply a multi-controlled Pauli-X (Toffoli / MCX) gate. Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`x` """ def y(self, q: int) -> None: @@ -661,26 +597,26 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def cy(self, control: Control | int, target: int) -> None: + def cy(self, control: operations.Control | int, target: int) -> None: """Apply a controlled Pauli-Y gate. + See Also: + :meth:`y` + Args: control: The control qubit target: The target qubit - - See Also: - :meth:`y` """ - def mcy(self, controls: set[Control | int], target: int) -> None: + def mcy(self, controls: AbstractSet[operations.Control | int], target: int) -> None: """Apply a multi-controlled Pauli-Y gate. + See Also: + :meth:`y` + Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`y` """ def z(self, q: int) -> None: @@ -693,26 +629,26 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def cz(self, control: Control | int, target: int) -> None: - """Apply a controlled Pauli-Z (CZ) gate. + def cz(self, control: operations.Control | int, target: int) -> None: + """Apply a controlled Pauli-Z gate. + + See Also: + :meth:`z` Args: control: The control qubit target: The target qubit + """ + + def mcz(self, controls: AbstractSet[operations.Control | int], target: int) -> None: + """Apply a multi-controlled Pauli-Z gate. See Also: :meth:`z` - """ - - def mcz(self, controls: set[Control | int], target: int) -> None: - """Apply a multi-controlled Pauli-Z (MCZ) gate. Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`z` """ def h(self, q: int) -> None: @@ -725,30 +661,30 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def ch(self, control: Control | int, target: int) -> None: + def ch(self, control: operations.Control | int, target: int) -> None: """Apply a controlled Hadamard gate. + See Also: + :meth:`h` + Args: control: The control qubit target: The target qubit - - See Also: - :meth:`h` """ - def mch(self, controls: set[Control | int], target: int) -> None: + def mch(self, controls: AbstractSet[operations.Control | int], target: int) -> None: """Apply a multi-controlled Hadamard gate. + See Also: + :meth:`h` + Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`h` """ def s(self, q: int) -> None: - r"""Apply an S gate (phase gate). + r"""Apply an S (i.e., phase) gate. .. math:: S = \begin{pmatrix} 1 & 0 \\ 0 & i \end{pmatrix} @@ -757,122 +693,122 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def cs(self, control: Control | int, target: int) -> None: - """Apply a controlled S gate (CS gate). + def cs(self, control: operations.Control | int, target: int) -> None: + """Apply a controlled S gate. + + See Also: + :meth:`s` Args: control: The control qubit target: The target qubit - - See Also: - :meth:`s` """ - def mcs(self, controls: set[Control | int], target: int) -> None: + def mcs(self, controls: AbstractSet[operations.Control | int], target: int) -> None: """Apply a multi-controlled S gate. + See Also: + :meth:`s` + Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`s` """ def sdg(self, q: int) -> None: - r"""Apply an :math:`S^{\dagger}` gate. + r"""Apply an :math:`S^\dagger` gate. .. math:: - S^{\dagger} = \begin{pmatrix} 1 & 0 \\ 0 & -i \end{pmatrix} + S^\dagger = \begin{pmatrix} 1 & 0 \\ 0 & -i \end{pmatrix} Args: q: The target qubit """ - def csdg(self, control: Control | int, target: int) -> None: - r"""Apply a controlled :math:`S^{\dagger}` gate. + def csdg(self, control: operations.Control | int, target: int) -> None: + r"""Apply a controlled :math:`S^\dagger` gate. + + See Also: + :meth:`sdg` Args: control: The control qubit target: The target qubit + """ + + def mcsdg(self, controls: AbstractSet[operations.Control | int], target: int) -> None: + r"""Apply a multi-controlled :math:`S^\dagger` gate. See Also: :meth:`sdg` - """ - - def mcsdg(self, controls: set[Control | int], target: int) -> None: - r"""Apply a multi-controlled :math:`S^{\dagger}` gate. Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`sdg` """ def t(self, q: int) -> None: r"""Apply a T gate. .. math:: - T = \begin{pmatrix} 1 & 0 \\ 0 & e^{i\pi/4} \end{pmatrix} + T = \begin{pmatrix} 1 & 0 \\ 0 & e^{i \pi / 4} \end{pmatrix} Args: q: The target qubit """ - def ct(self, control: Control | int, target: int) -> None: + def ct(self, control: operations.Control | int, target: int) -> None: """Apply a controlled T gate. + See Also: + :meth:`t` + Args: control: The control qubit target: The target qubit - - See Also: - :meth:`t` """ - def mct(self, controls: set[Control | int], target: int) -> None: + def mct(self, controls: AbstractSet[operations.Control | int], target: int) -> None: """Apply a multi-controlled T gate. + See Also: + :meth:`t` + Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`t` """ def tdg(self, q: int) -> None: - r"""Apply a :math:`T^{\dagger}` gate. + r"""Apply a :math:`T^\dagger` gate. .. math:: - T^{\dagger} = \begin{pmatrix} 1 & 0 \\ 0 & e^{-i\pi/4} \end{pmatrix} + T^\dagger = \begin{pmatrix} 1 & 0 \\ 0 & e^{-i \pi / 4} \end{pmatrix} Args: q: The target qubit """ - def ctdg(self, control: Control | int, target: int) -> None: - r"""Apply a controlled :math:`T^{\dagger}` gate. + def ctdg(self, control: operations.Control | int, target: int) -> None: + r"""Apply a controlled :math:`T^\dagger` gate. + + See Also: + :meth:`tdg` Args: control: The control qubit target: The target qubit + """ + + def mctdg(self, controls: AbstractSet[operations.Control | int], target: int) -> None: + r"""Apply a multi-controlled :math:`T^\dagger` gate. See Also: :meth:`tdg` - """ - - def mctdg(self, controls: set[Control | int], target: int) -> None: - r"""Apply a multi-controlled :math:`T^{\dagger}` gate. Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`tdg` """ def v(self, q: int) -> None: @@ -885,58 +821,58 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def cv(self, control: Control | int, target: int) -> None: + def cv(self, control: operations.Control | int, target: int) -> None: """Apply a controlled V gate. + See Also: + :meth:`v` + Args: control: The control qubit target: The target qubit - - See Also: - :meth:`v` """ - def mcv(self, controls: set[Control | int], target: int) -> None: + def mcv(self, controls: AbstractSet[operations.Control | int], target: int) -> None: """Apply a multi-controlled V gate. + See Also: + :meth:`v` + Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`v` """ def vdg(self, q: int) -> None: - r"""Apply a :math:`V^{\dagger}` gate. + r"""Apply a :math:`V^\dagger` gate. .. math:: - V^{\dagger} = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & i \\ i & 1 \end{pmatrix} + V^\dagger = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & i \\ i & 1 \end{pmatrix} Args: q: The target qubit """ - def cvdg(self, control: Control | int, target: int) -> None: - r"""Apply a controlled :math:`V^{\dagger}` gate. + def cvdg(self, control: operations.Control | int, target: int) -> None: + r"""Apply a controlled :math:`V^\dagger` gate. + + See Also: + :meth:`vdg` Args: control: The control qubit target: The target qubit + """ + + def mcvdg(self, controls: AbstractSet[operations.Control | int], target: int) -> None: + r"""Apply a multi-controlled :math:`V^\dagger` gate. See Also: :meth:`vdg` - """ - - def mcvdg(self, controls: set[Control | int], target: int) -> None: - r"""Apply a multi-controlled :math:`V^{\dagger}` gate. Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`vdg` """ def sx(self, q: int) -> None: @@ -949,30 +885,30 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def csx(self, control: Control | int, target: int) -> None: + def csx(self, control: operations.Control | int, target: int) -> None: r"""Apply a controlled :math:`\sqrt{X}` gate. + See Also: + :meth:`sx` + Args: control: The control qubit target: The target qubit - - See Also: - :meth:`sx` """ - def mcsx(self, controls: set[Control | int], target: int) -> None: + def mcsx(self, controls: AbstractSet[operations.Control | int], target: int) -> None: r"""Apply a multi-controlled :math:`\sqrt{X}` gate. + See Also: + :meth:`sx` + Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`sx` """ def sxdg(self, q: int) -> None: - r"""Apply a :math:`\sqrt{X}^{\dagger}` gate. + r"""Apply a :math:`\sqrt{X}^\dagger` gate. .. math:: \sqrt{X}^{\dagger} = \frac{1}{2} \begin{pmatrix} 1 - i & 1 + i \\ 1 + i & 1 - i \end{pmatrix} @@ -981,218 +917,183 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def csxdg(self, control: Control | int, target: int) -> None: - r"""Apply a controlled :math:`\sqrt{X}^{\dagger}` gate. + def csxdg(self, control: operations.Control | int, target: int) -> None: + r"""Apply a controlled :math:`\sqrt{X}^\dagger` gate. + + See Also: + :meth:`sxdg` Args: control: The control qubit target: The target qubit + """ + + def mcsxdg(self, controls: AbstractSet[operations.Control | int], target: int) -> None: + r"""Apply a multi-controlled :math:`\sqrt{X}^\dagger` gate. See Also: :meth:`sxdg` - """ - - def mcsxdg(self, controls: set[Control | int], target: int) -> None: - r"""Apply a multi-controlled :math:`\sqrt{X}^{\dagger}` gate. Args: controls: The control qubits target: The target qubit - - See Also: - :meth:`sxdg` """ - def r(self, theta: float | Expression, phi: float | Expression, q: int) -> None: - r"""Apply an :math:`R(\theta, \phi)` gate. + def rx(self, theta: symbolic.Expression | float, q: int) -> None: + r"""Apply an :math:`R_x(\theta)` gate. .. math:: - R(\theta, \phi) = e^{-i\frac{\theta}{2}(\cos(\phi)X+\sin(\phi)Y)} - = \begin{pmatrix} \cos(\theta/2) & -i e^{-i\phi} \sin(\theta/2) \\ - -i e^{i\phi} \sin(\theta/2) & \cos(\theta/2) \end{pmatrix} + R_x(\theta) = e^{-i \theta X / 2} = \cos(\theta / 2) I - i \sin(\theta / 2) X + = \begin{pmatrix} \cos(\theta / 2) & -i \sin(\theta / 2) \\ -i \sin(\theta / 2) & \cos(\theta / 2) \end{pmatrix} Args: - theta: The rotation angle :math:`\theta` - phi: The angle specifying the rotation axis given by :math:`\cos(\phi)X+\sin(\phi)Y` + theta: The rotation angle q: The target qubit """ - def cr(self, theta: float | Expression, phi: float | Expression, control: Control | int, target: int) -> None: - r"""Apply a controlled :math:`R(\theta, \phi)` gate. + def crx(self, theta: symbolic.Expression | float, control: operations.Control | int, target: int) -> None: + r"""Apply a controlled :math:`R_x(\theta)` gate. + + See Also: + :meth:`rx` Args: - theta: The rotation angle :math:`\theta` - phi: The angle specifying the rotation axis given by :math:`\cos(\phi)X+\sin(\phi)Y` + theta: The rotation angle control: The control qubit target: The target qubit - - See Also: - :meth:`r` """ - def mcr( - self, theta: float | Expression, phi: float | Expression, controls: set[Control | int], target: int + def mcrx( + self, theta: symbolic.Expression | float, controls: AbstractSet[operations.Control | int], target: int ) -> None: - r"""Apply a multi-controlled :math:`R(\theta, \phi)` gate. - - Args: - theta: The rotation angle :math:`\theta` - phi: The angle specifying the rotation axis given by :math:`\cos(\phi)X+\sin(\phi)Y` - controls: The control qubits - target: The target qubit - - See Also: - :meth:`r` - """ - - def rx(self, theta: float | Expression, q: int) -> None: - r"""Apply an :math:`R_x(\theta)` gate. - - .. math:: - R_x(\theta) = e^{-i\theta X/2} = \cos(\theta/2) I - i \sin(\theta/2) X - = \begin{pmatrix} \cos(\theta/2) & -i \sin(\theta/2) \\ -i \sin(\theta/2) & \cos(\theta/2) \end{pmatrix} - - Args: - theta: The rotation angle - q: The target qubit - """ - - def crx(self, theta: float | Expression, control: Control | int, target: int) -> None: - r"""Apply a controlled :math:`R_x(\theta)` gate. - - Args: - theta: The rotation angle - control: The control qubit - target: The target qubit + r"""Apply a multi-controlled :math:`R_x(\theta)` gate. See Also: :meth:`rx` - """ - - def mcrx(self, theta: float | Expression, controls: set[Control | int], target: int) -> None: - r"""Apply a multi-controlled :math:`R_x(\theta)` gate. Args: theta: The rotation angle controls: The control qubits target: The target qubit - - See Also: - :meth:`rx` """ - def ry(self, theta: float | Expression, q: int) -> None: + def ry(self, theta: symbolic.Expression | float, q: int) -> None: r"""Apply an :math:`R_y(\theta)` gate. .. math:: - R_y(\theta) = e^{-i\theta Y/2} = \cos(\theta/2) I - i \sin(\theta/2) Y - = \begin{pmatrix} \cos(\theta/2) & -\sin(\theta/2) \\ \sin(\theta/2) & \cos(\theta/2) \end{pmatrix} + R_y(\theta) = e^{-i \theta Y / 2} = \cos(\theta / 2) I - i \sin(\theta / 2) Y + = \begin{pmatrix} \cos(\theta / 2) & -\sin(\theta / 2) \\ \sin(\theta / 2) & \cos(\theta / 2) \end{pmatrix} Args: theta: The rotation angle q: The target qubit """ - def cry(self, theta: float | Expression, control: Control | int, target: int) -> None: + def cry(self, theta: symbolic.Expression | float, control: operations.Control | int, target: int) -> None: r"""Apply a controlled :math:`R_y(\theta)` gate. + See Also: + :meth:`ry` + Args: theta: The rotation angle control: The control qubit target: The target qubit - - See Also: - :meth:`ry` """ - def mcry(self, theta: float | Expression, controls: set[Control | int], target: int) -> None: + def mcry( + self, theta: symbolic.Expression | float, controls: AbstractSet[operations.Control | int], target: int + ) -> None: r"""Apply a multi-controlled :math:`R_y(\theta)` gate. + See Also: + :meth:`ry` + Args: theta: The rotation angle controls: The control qubits target: The target qubit - - See Also: - :meth:`ry` """ - def rz(self, theta: float | Expression, q: int) -> None: + def rz(self, theta: symbolic.Expression | float, q: int) -> None: r"""Apply an :math:`R_z(\theta)` gate. .. math:: - R_z(\theta) = e^{-i\theta Z/2} = \begin{pmatrix} e^{-i\theta/2} & 0 \\ 0 & e^{i\theta/2} \end{pmatrix} + R_z(\theta) = e^{-i \theta Z / 2} = \begin{pmatrix} e^{-i \theta / 2} & 0 \\ 0 & e^{i \theta / 2} \end{pmatrix} Args: theta: The rotation angle q: The target qubit """ - def crz(self, theta: float | Expression, control: Control | int, target: int) -> None: + def crz(self, theta: symbolic.Expression | float, control: operations.Control | int, target: int) -> None: r"""Apply a controlled :math:`R_z(\theta)` gate. + See Also: + :meth:`rz` + Args: theta: The rotation angle control: The control qubit target: The target qubit - - See Also: - :meth:`rz` """ - def mcrz(self, theta: float | Expression, controls: set[Control | int], target: int) -> None: + def mcrz( + self, theta: symbolic.Expression | float, controls: AbstractSet[operations.Control | int], target: int + ) -> None: r"""Apply a multi-controlled :math:`R_z(\theta)` gate. + See Also: + :meth:`rz` + Args: theta: The rotation angle controls: The control qubits target: The target qubit - - See Also: - :meth:`rz` """ - def p(self, theta: float | Expression, q: int) -> None: + def p(self, theta: symbolic.Expression | float, q: int) -> None: r"""Apply a phase gate. .. math:: - P(\theta) = \begin{pmatrix} 1 & 0 \\ 0 & e^{i\theta} \end{pmatrix} + P(\theta) = \begin{pmatrix} 1 & 0 \\ 0 & e^{i \theta} \end{pmatrix} Args: theta: The rotation angle q: The target qubit """ - def cp(self, theta: float | Expression, control: Control | int, target: int) -> None: + def cp(self, theta: symbolic.Expression | float, control: operations.Control | int, target: int) -> None: """Apply a controlled phase gate. + See Also: + :meth:`p` + Args: theta: The rotation angle control: The control qubit target: The target qubit - - See Also: - :meth:`p` """ - def mcp(self, theta: float | Expression, controls: set[Control | int], target: int) -> None: + def mcp( + self, theta: symbolic.Expression | float, controls: AbstractSet[operations.Control | int], target: int + ) -> None: """Apply a multi-controlled phase gate. + See Also: + :meth:`p` + Args: theta: The rotation angle controls: The control qubits target: The target qubit - - See Also: - :meth:`p` """ - def u2(self, phi: float | Expression, lambda_: float | Expression, q: int) -> None: + def u2(self, phi: symbolic.Expression | float, lambda_: symbolic.Expression | float, q: int) -> None: r"""Apply a :math:`U_2(\phi, \lambda)` gate. .. math:: - U_2(\phi, \lambda) = - \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & -e^{i\lambda} \\ e^{i\phi} & e^{i(\phi + \lambda)} \end{pmatrix} + U_2(\phi, \lambda) = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & -e^{i \lambda} \\ e^{i \phi} & e^{i (\phi + \lambda)} \end{pmatrix} Args: phi: The rotation angle @@ -1200,45 +1101,106 @@ class QuantumComputation(MutableSequence[Operation]): q: The target qubit """ - def cu2(self, phi: float | Expression, lambda_: float | Expression, control: Control | int, target: int) -> None: + def cu2( + self, + phi: symbolic.Expression | float, + lambda_: symbolic.Expression | float, + control: operations.Control | int, + target: int, + ) -> None: r"""Apply a controlled :math:`U_2(\phi, \lambda)` gate. + See Also: + :meth:`u2` + Args: phi: The rotation angle lambda_: The rotation angle control: The control qubit target: The target qubit - - See Also: - :meth:`u2` """ def mcu2( self, - phi: float | Expression, - lambda_: float | Expression, - controls: set[Control | int], + phi: symbolic.Expression | float, + lambda_: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], target: int, ) -> None: r"""Apply a multi-controlled :math:`U_2(\phi, \lambda)` gate. + See Also: + :meth:`u2` + Args: phi: The rotation angle lambda_: The rotation angle controls: The control qubits target: The target qubit + """ + + def r(self, theta: symbolic.Expression | float, phi: symbolic.Expression | float, q: int) -> None: + r"""Apply an :math:`R(\theta, \phi)` gate. + + .. math:: + R(\theta, \phi) = e^{-i \frac{\theta}{2} (\cos(\phi) X + \sin(\phi) Y)} + = \begin{pmatrix} \cos(\theta / 2) & -i e^{-i \phi} \sin(\theta / 2) \\ -i e^{i \phi} \sin(\theta / 2) & \cos(\theta / 2) \end{pmatrix} + + Args: + theta: The rotation angle + phi: The rotation angle + q: The target qubit + """ + + def cr( + self, + theta: symbolic.Expression | float, + phi: symbolic.Expression | float, + control: operations.Control | int, + target: int, + ) -> None: + r"""Apply a controlled :math:`R(\theta, \phi)` gate. See Also: - :meth:`u2` + :meth:`r` + + Args: + theta: The rotation angle + phi: The rotation angle + control: The control qubit + target: The target qubit """ - def u(self, theta: float | Expression, phi: float | Expression, lambda_: float | Expression, q: int) -> None: + def mcr( + self, + theta: symbolic.Expression | float, + phi: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], + target: int, + ) -> None: + r"""Apply a multi-controlled :math:`R(\theta, \phi)` gate. + + See Also: + :meth:`r` + + Args: + theta: The rotation angle + phi: The rotation angle + controls: The control qubits + target: The target qubit + """ + + def u( + self, + theta: symbolic.Expression | float, + phi: symbolic.Expression | float, + lambda_: symbolic.Expression | float, + q: int, + ) -> None: r"""Apply a :math:`U(\theta, \phi, \lambda)` gate. .. math:: - U(\theta, \phi, \lambda) = - \begin{pmatrix} \cos(\theta/2) & -e^{i\lambda}\sin(\theta/2) \\ - e^{i\phi}\sin(\theta/2) & e^{i(\phi + \lambda)}\cos(\theta/2) \end{pmatrix} + U(\theta, \phi, \lambda) = \begin{pmatrix} \cos(\theta / 2) & -e^{i \lambda} \sin(\theta / 2) \\ e^{i \phi} \sin(\theta / 2) & e^{i (\phi + \lambda)}\cos(\theta / 2) \end{pmatrix} Args: theta: The rotation angle @@ -1249,83 +1211,83 @@ class QuantumComputation(MutableSequence[Operation]): def cu( self, - theta: float | Expression, - phi: float | Expression, - lambda_: float | Expression, - control: Control | int, + theta: symbolic.Expression | float, + phi: symbolic.Expression | float, + lambda_: symbolic.Expression | float, + control: operations.Control | int, target: int, ) -> None: r"""Apply a controlled :math:`U(\theta, \phi, \lambda)` gate. + See Also: + :meth:`u` + Args: theta: The rotation angle phi: The rotation angle lambda_: The rotation angle control: The control qubit target: The target qubit - - See Also: - :meth:`u` """ def mcu( self, - theta: float | Expression, - phi: float | Expression, - lambda_: float | Expression, - controls: set[Control | int], + theta: symbolic.Expression | float, + phi: symbolic.Expression | float, + lambda_: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], target: int, ) -> None: r"""Apply a multi-controlled :math:`U(\theta, \phi, \lambda)` gate. + See Also: + :meth:`u` + Args: theta: The rotation angle phi: The rotation angle lambda_: The rotation angle controls: The control qubits target: The target qubit - - See Also: - :meth:`u` """ def swap(self, target1: int, target2: int) -> None: r"""Apply a SWAP gate. .. math:: - SWAP = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} + \text{SWAP} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} Args: target1: The first target qubit target2: The second target qubit """ - def cswap(self, control: Control | int, target1: int, target2: int) -> None: + def cswap(self, control: operations.Control | int, target1: int, target2: int) -> None: """Apply a controlled SWAP gate. + See Also: + :meth:`swap` + Args: control: The control qubit target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`swap` """ - def mcswap(self, controls: set[Control | int], target1: int, target2: int) -> None: + def mcswap(self, controls: AbstractSet[operations.Control | int], target1: int, target2: int) -> None: """Apply a multi-controlled SWAP gate. + See Also: + :meth:`swap` + Args: controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`swap` """ def dcx(self, target1: int, target2: int) -> None: - r"""Apply a DCX (double CNOT) gate. + r"""Apply a DCX (i.e., double CNOT) gate. .. math:: DCX = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \end{pmatrix} @@ -1335,217 +1297,214 @@ class QuantumComputation(MutableSequence[Operation]): target2: The second target qubit """ - def cdcx(self, control: Control | int, target1: int, target2: int) -> None: - """Apply a controlled DCX (double CNOT) gate. + def cdcx(self, control: operations.Control | int, target1: int, target2: int) -> None: + """Apply a controlled DCX gate. + + See Also: + :meth:`dcx` Args: control: The control qubit target1: The first target qubit target2: The second target qubit + """ + + def mcdcx(self, controls: AbstractSet[operations.Control | int], target1: int, target2: int) -> None: + """Apply a multi-controlled DCX gate. See Also: :meth:`dcx` - """ - - def mcdcx(self, controls: set[Control | int], target1: int, target2: int) -> None: - """Apply a multi-controlled DCX (double CNOT) gate. Args: controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`dcx` """ def ecr(self, target1: int, target2: int) -> None: - r"""Apply an ECR (echoed cross-resonance) gate. + r"""Apply a ECR (echoed cross-resonance) gate. .. math:: - ECR = \frac{1}{\sqrt{2}} - \begin{pmatrix} 0 & 0 & 1 & i \\ 0 & 0 & i & 1 \\ 1 & -i & 0 & 0 \\ -i & 1 & 0 & 0 \end{pmatrix} + ECR = \frac{1}{\sqrt{2}} \begin{pmatrix} 0 & 0 & 1 & i \\ 0 & 0 & i & 1 \\ 1 & -i & 0 & 0 \\ -i & 1 & 0 & 0 \end{pmatrix} Args: target1: The first target qubit target2: The second target qubit """ - def cecr(self, control: Control | int, target1: int, target2: int) -> None: - """Apply a controlled ECR (echoed cross-resonance) gate. + def cecr(self, control: operations.Control | int, target1: int, target2: int) -> None: + """Apply a controlled ECR gate. + + See Also: + :meth:`ecr` Args: control: The control qubit target1: The first target qubit target2: The second target qubit + """ + + def mcecr(self, controls: AbstractSet[operations.Control | int], target1: int, target2: int) -> None: + """Apply a multi-controlled ECR gate. See Also: :meth:`ecr` - """ - - def mcecr(self, controls: set[Control | int], target1: int, target2: int) -> None: - """Apply a multi-controlled ECR (echoed cross-resonance) gate. Args: controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`ecr` """ def iswap(self, target1: int, target2: int) -> None: - r"""Apply an iSWAP gate. + r"""Apply a :math:`i\text{SWAP}` gate. .. math:: - iSWAP = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & i & 0 \\ 0 & i & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} + i\text{SWAP} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & i & 0 \\ 0 & i & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} Args: target1: The first target qubit target2: The second target qubit """ - def ciswap(self, control: Control | int, target1: int, target2: int) -> None: - """Apply a controlled iSWAP gate. + def ciswap(self, control: operations.Control | int, target1: int, target2: int) -> None: + r"""Apply a controlled :math:`i\text{SWAP}` gate. + + See Also: + :meth:`iswap` Args: control: The control qubit target1: The first target qubit target2: The second target qubit + """ + + def mciswap(self, controls: AbstractSet[operations.Control | int], target1: int, target2: int) -> None: + r"""Apply a multi-controlled :math:`i\text{SWAP}` gate. See Also: :meth:`iswap` - """ - - def mciswap(self, controls: set[Control | int], target1: int, target2: int) -> None: - """Apply a multi-controlled iSWAP gate. Args: controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`iswap` """ def iswapdg(self, target1: int, target2: int) -> None: - r"""Apply an :math:`iSWAP^{\dagger}` gate. + r"""Apply a :math:`i\text{SWAP}^\dagger` gate. .. math:: - iSWAP^{\dagger} = - \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & -i & 0 \\ 0 & -i & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} + i\text{SWAP}^\dagger = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & -i & 0 \\ 0 & -i & 0 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} Args: target1: The first target qubit target2: The second target qubit """ - def ciswapdg(self, control: Control | int, target1: int, target2: int) -> None: - r"""Apply a controlled :math:`iSWAP^{\dagger}` gate. + def ciswapdg(self, control: operations.Control | int, target1: int, target2: int) -> None: + r"""Apply a controlled :math:`i\text{SWAP}^\dagger` gate. + + See Also: + :meth:`iswapdg` Args: control: The control qubit target1: The first target qubit target2: The second target qubit + """ + + def mciswapdg(self, controls: AbstractSet[operations.Control | int], target1: int, target2: int) -> None: + r"""Apply a multi-controlled :math:`i\text{SWAP}^\dagger` gate. See Also: :meth:`iswapdg` - """ - - def mciswapdg(self, controls: set[Control | int], target1: int, target2: int) -> None: - r"""Apply a multi-controlled :math:`iSWAP^{\dagger}` gate. Args: controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`iswapdg` """ def peres(self, target1: int, target2: int) -> None: r"""Apply a Peres gate. .. math:: - Peres = \begin{pmatrix} 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \end{pmatrix} + \text{Peres} = \begin{pmatrix} 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \end{pmatrix} Args: target1: The first target qubit target2: The second target qubit """ - def cperes(self, control: Control | int, target1: int, target2: int) -> None: + def cperes(self, control: operations.Control | int, target1: int, target2: int) -> None: """Apply a controlled Peres gate. + See Also: + :meth:`peres` + Args: control: The control qubit target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`peres` """ - def mcperes(self, controls: set[Control | int], target1: int, target2: int) -> None: + def mcperes(self, controls: AbstractSet[operations.Control | int], target1: int, target2: int) -> None: """Apply a multi-controlled Peres gate. + See Also: + :meth:`peres` + Args: controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`peres` """ def peresdg(self, target1: int, target2: int) -> None: - r"""Apply a :math:`Peres^{\dagger}` gate. + r"""Apply a :math:`\text{Peres}^\dagger` gate. .. math:: - Peres^{\dagger} = - \begin{pmatrix} 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 1 & 0 & 0 \\ 1 & 0 & 0 & 0 \end{pmatrix} + \text{Peres}^\dagger = \begin{pmatrix} 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \end{pmatrix} Args: target1: The first target qubit target2: The second target qubit """ - def cperesdg(self, control: Control | int, target1: int, target2: int) -> None: - r"""Apply a controlled :math:`Peres^{\dagger}` gate. + def cperesdg(self, control: operations.Control | int, target1: int, target2: int) -> None: + r"""Apply a controlled :math:`\text{Peres}^\dagger` gate. + + See Also: + :meth:`peresdg` Args: control: The control qubit target1: The first target qubit target2: The second target qubit + """ + + def mcperesdg(self, controls: AbstractSet[operations.Control | int], target1: int, target2: int) -> None: + r"""Apply a multi-controlled :math:`\text{Peres}^\dagger` gate. See Also: :meth:`peresdg` - """ - - def mcperesdg(self, controls: set[Control | int], target1: int, target2: int) -> None: - r"""Apply a multi-controlled :math:`Peres^{\dagger}` gate. Args: controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`peresdg` """ - def rxx(self, theta: float | Expression, target1: int, target2: int) -> None: + def rxx(self, theta: symbolic.Expression | float, target1: int, target2: int) -> None: r"""Apply an :math:`R_{xx}(\theta)` gate. .. math:: - R_{xx}(\theta) = e^{-i\theta XX/2} = \cos(\theta/2) I\otimes I - i \sin(\theta/2) X \otimes X - = \begin{pmatrix} \cos(\theta/2) & 0 & 0 & -i \sin(\theta/2) \\ - 0 & \cos(\theta/2) & -i \sin(\theta/2) & 0 \\ - 0 & -i \sin(\theta/2) & \cos(\theta/2) & 0 \\ - -i \sin(\theta/2) & 0 & 0 & \cos(\theta/2) \end{pmatrix} + R_{xx}(\theta) = e^{-i \theta XX / 2} = \cos(\theta / 2) I \otimes I - i \sin(\theta / 2) X \otimes X + = \begin{pmatrix} \cos(\theta / 2) & 0 & 0 & -i \sin(\theta / 2) \\ + 0 & \cos(\theta / 2) & -i \sin(\theta / 2) & 0 \\ + 0 & -i \sin(\theta / 2) & \cos(\theta / 2) & 0 \\ + -i \sin(\theta / 2) & 0 & 0 & \cos(\theta / 2) \end{pmatrix} Args: theta: The rotation angle @@ -1553,41 +1512,49 @@ class QuantumComputation(MutableSequence[Operation]): target2: The second target qubit """ - def crxx(self, theta: float | Expression, control: Control | int, target1: int, target2: int) -> None: + def crxx( + self, theta: symbolic.Expression | float, control: operations.Control | int, target1: int, target2: int + ) -> None: r"""Apply a controlled :math:`R_{xx}(\theta)` gate. + See Also: + :meth:`rxx` + Args: theta: The rotation angle control: The control qubit target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`rxx` """ - def mcrxx(self, theta: float | Expression, controls: set[Control | int], target1: int, target2: int) -> None: + def mcrxx( + self, + theta: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], + target1: int, + target2: int, + ) -> None: r"""Apply a multi-controlled :math:`R_{xx}(\theta)` gate. + See Also: + :meth:`rxx` + Args: theta: The rotation angle controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`rxx` """ - def ryy(self, theta: float | Expression, target1: int, target2: int) -> None: + def ryy(self, theta: symbolic.Expression | float, target1: int, target2: int) -> None: r"""Apply an :math:`R_{yy}(\theta)` gate. .. math:: - R_{yy}(\theta) = e^{-i\theta YY/2} = \cos(\theta/2) I\otimes I - i \sin(\theta/2) Y \otimes Y - = \begin{pmatrix} \cos(\theta/2) & 0 & 0 & i \sin(\theta/2) \\ - 0 & \cos(\theta/2) & -i \sin(\theta/2) & 0 \\ - 0 & -i \sin(\theta/2) & \cos(\theta/2) & 0 \\ - i \sin(\theta/2) & 0 & 0 & \cos(\theta/2) \end{pmatrix} + R_{yy}(\theta) = e^{-i \theta YY / 2} = \cos(\theta / 2) I \otimes I - i \sin(\theta / 2) Y \otimes Y + = \begin{pmatrix} \cos(\theta / 2) & 0 & 0 & i \sin(\theta / 2) \\ + 0 & \cos(\theta / 2) & -i \sin(\theta / 2) & 0 \\ + 0 & -i \sin(\theta / 2) & \cos(\theta / 2) & 0 \\ + i \sin(\theta / 2) & 0 & 0 & \cos(\theta / 2) \end{pmatrix} Args: theta: The rotation angle @@ -1595,41 +1562,49 @@ class QuantumComputation(MutableSequence[Operation]): target2: The second target qubit """ - def cryy(self, theta: float | Expression, control: Control | int, target1: int, target2: int) -> None: + def cryy( + self, theta: symbolic.Expression | float, control: operations.Control | int, target1: int, target2: int + ) -> None: r"""Apply a controlled :math:`R_{yy}(\theta)` gate. + See Also: + :meth:`ryy` + Args: theta: The rotation angle control: The control qubit target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`ryy` """ - def mcryy(self, theta: float | Expression, controls: set[Control | int], target1: int, target2: int) -> None: + def mcryy( + self, + theta: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], + target1: int, + target2: int, + ) -> None: r"""Apply a multi-controlled :math:`R_{yy}(\theta)` gate. + See Also: + :meth:`ryy` + Args: theta: The rotation angle controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`ryy` """ - def rzz(self, theta: float | Expression, target1: int, target2: int) -> None: - r"""Apply an :math:`R_{zz}(\theta)` gate. + def rzx(self, theta: symbolic.Expression | float, target1: int, target2: int) -> None: + r"""Apply an :math:`R_{zx}(\theta)` gate. .. math:: - R_{zz}(\theta) = e^{-i\theta ZZ/2} = - \begin{pmatrix} e^{-i\theta/2} & 0 & 0 & 0 \\ - 0 & e^{i\theta/2} & 0 & 0 \\ - 0 & 0 & e^{i\theta/2} & 0 \\ - 0 & 0 & 0 & e^{-i\theta/2} \end{pmatrix} + R_{zx}(\theta) = e^{-i \theta ZX / 2} = \cos(\theta / 2) I \otimes I - i \sin(\theta / 2) Z \otimes X + = \begin{pmatrix} \cos(\theta/2) & -i \sin(\theta/2) & 0 & 0 \\ + -i \sin(\theta/2) & \cos(\theta/2) & 0 & 0 \\ + 0 & 0 & \cos(\theta/2) & i \sin(\theta/2) \\ + 0 & 0 & i \sin(\theta/2) & \cos(\theta/2) \end{pmatrix} Args: theta: The rotation angle @@ -1637,41 +1612,49 @@ class QuantumComputation(MutableSequence[Operation]): target2: The second target qubit """ - def crzz(self, theta: float | Expression, control: Control | int, target1: int, target2: int) -> None: - r"""Apply a controlled :math:`R_{zz}(\theta)` gate. + def crzx( + self, theta: symbolic.Expression | float, control: operations.Control | int, target1: int, target2: int + ) -> None: + r"""Apply a controlled :math:`R_{zx}(\theta)` gate. + + See Also: + :meth:`rzx` Args: theta: The rotation angle control: The control qubit target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`rzz` """ - def mcrzz(self, theta: float | Expression, controls: set[Control | int], target1: int, target2: int) -> None: - r"""Apply a multi-controlled :math:`R_{zz}(\theta)` gate. + def mcrzx( + self, + theta: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], + target1: int, + target2: int, + ) -> None: + r"""Apply a multi-controlled :math:`R_{zx}(\theta)` gate. + + See Also: + :meth:`rzx` Args: theta: The rotation angle controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`rzz` """ - def rzx(self, theta: float | Expression, target1: int, target2: int) -> None: - r"""Apply an :math:`R_{zx}(\theta)` gate. + def rzz(self, theta: symbolic.Expression | float, target1: int, target2: int) -> None: + r"""Apply an :math:`R_{zz}(\theta)` gate. .. math:: - R_{zx}(\theta) = e^{-i\theta ZX/2} = \cos(\theta/2) I\otimes I - i \sin(\theta/2) Z \otimes X = - \begin{pmatrix} \cos(\theta/2) & -i \sin(\theta/2) & 0 & 0 \\ - -i \sin(\theta/2) & \cos(\theta/2) & 0 & 0 \\ - 0 & 0 & \cos(\theta/2) & i \sin(\theta/2) \\ - 0 & 0 & i \sin(\theta/2) & \cos(\theta/2) \end{pmatrix} + R_{zz}(\theta) = e^{-i \theta ZZ / 2} + = \begin{pmatrix} e^{-i \theta / 2} & 0 & 0 & 0 \\ + 0 & e^{i \theta / 2} & 0 & 0 \\ + 0 & 0 & e^{i \theta / 2} & 0 \\ + 0 & 0 & 0 & e^{-i \theta / 2} \end{pmatrix} Args: theta: The rotation angle @@ -1679,42 +1662,51 @@ class QuantumComputation(MutableSequence[Operation]): target2: The second target qubit """ - def crzx(self, theta: float | Expression, control: Control | int, target1: int, target2: int) -> None: - r"""Apply a controlled :math:`R_{zx}(\theta)` gate. + def crzz( + self, theta: symbolic.Expression | float, control: operations.Control | int, target1: int, target2: int + ) -> None: + r"""Apply a controlled :math:`R_{zz}(\theta)` gate. + + See Also: + :meth:`rzz` Args: theta: The rotation angle control: The control qubit target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`rzx` """ - def mcrzx(self, theta: float | Expression, controls: set[Control | int], target1: int, target2: int) -> None: - r"""Apply a multi-controlled :math:`R_{zx}(\theta)` gate. + def mcrzz( + self, + theta: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], + target1: int, + target2: int, + ) -> None: + r"""Apply a multi-controlled :math:`R_{zz}(\theta)` gate. + + See Also: + :meth:`rzz` Args: theta: The rotation angle controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`rzx` """ - def xx_minus_yy(self, theta: float | Expression, beta: float | Expression, target1: int, target2: int) -> None: + def xx_minus_yy( + self, theta: symbolic.Expression | float, beta: symbolic.Expression | float, target1: int, target2: int + ) -> None: r"""Apply an :math:`R_{XX - YY}(\theta, \beta)` gate. .. math:: - R_{XX - YY}(\theta, \beta) - = R_{z_2}(\beta) \cdot e^{-i\frac{\theta}{2} \frac{XX-YY}{2}} \cdot R_{z_2}(-\beta) - = \begin{pmatrix} \cos(\theta/2) & 0 & 0 & -i \sin(\theta/2) e^{-i\beta} \\ - 0 & 1 & 0 & 0 \\ - 0 & 0 & 1 & 0 \\ - -i \sin(\theta/2) e^{i\beta} & 0 & 0 & \cos(\theta/2) \end{pmatrix} + R_{XX - YY}(\theta, \beta) = R_{z_2}(\beta) \cdot e^{-i \frac{\theta}{2} \frac{XX - YY}{2}} \cdot R_{z_2}(-\beta) + = \begin{pmatrix} \cos(\theta / 2) & 0 & 0 & -i \sin(\theta / 2) e^{-i \beta} \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + -i \sin(\theta / 2) e^{i \beta} & 0 & 0 & \cos(\theta / 2) \end{pmatrix} Args: theta: The rotation angle @@ -1725,56 +1717,57 @@ class QuantumComputation(MutableSequence[Operation]): def cxx_minus_yy( self, - theta: float | Expression, - beta: float | Expression, - control: Control | int, + theta: symbolic.Expression | float, + beta: symbolic.Expression | float, + control: operations.Control | int, target1: int, target2: int, ) -> None: r"""Apply a controlled :math:`R_{XX - YY}(\theta, \beta)` gate. + See Also: + :meth:`xx_minus_yy` + Args: theta: The rotation angle beta: The rotation angle control: The control qubit target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`xx_minus_yy` """ def mcxx_minus_yy( self, - theta: float | Expression, - beta: float | Expression, - controls: set[Control | int], + theta: symbolic.Expression | float, + beta: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], target1: int, target2: int, ) -> None: r"""Apply a multi-controlled :math:`R_{XX - YY}(\theta, \beta)` gate. + See Also: + :meth:`xx_minus_yy` + Args: theta: The rotation angle beta: The rotation angle controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`xx_minus_yy` """ - def xx_plus_yy(self, theta: float | Expression, beta: float | Expression, target1: int, target2: int) -> None: + def xx_plus_yy( + self, theta: symbolic.Expression | float, beta: symbolic.Expression | float, target1: int, target2: int + ) -> None: r"""Apply an :math:`R_{XX + YY}(\theta, \beta)` gate. .. math:: - R_{XX + YY}(\theta, \beta) - = R_{z_1}(\beta) \cdot e^{-i\frac{\theta}{2} \frac{XX+YY}{2}} \cdot R_{z_1}(-\beta) - = \begin{pmatrix} 1 & 0 & 0 & 0 \\ - 0 & \cos(\theta/2) & -i \sin(\theta/2) e^{-i\beta} & 0 \\ - 0 & -i \sin(\theta/2) e^{i\beta} & \cos(\theta/2) & 0 \\ - 0 & 0 & 0 & 1 \end{pmatrix} + R_{XX + YY}(\theta, \beta) = R_{z_1}(\beta) \cdot e^{-i \frac{\theta}{2} \frac{XX + YY}{2}} \cdot R_{z_1}(-\beta) + = \begin{pmatrix} 1 & 0 & 0 & 0 \\ + 0 & \cos(\theta / 2) & -i \sin(\theta / 2) e^{-i \beta} & 0 \\ + 0 & -i \sin(\theta / 2) e^{i \beta} & \cos(\theta / 2) & 0 \\ + 0 & 0 & 0 & 1 \end{pmatrix} Args: theta: The rotation angle @@ -1785,54 +1778,54 @@ class QuantumComputation(MutableSequence[Operation]): def cxx_plus_yy( self, - theta: float | Expression, - beta: float | Expression, - control: Control | int, + theta: symbolic.Expression | float, + beta: symbolic.Expression | float, + control: operations.Control | int, target1: int, target2: int, ) -> None: r"""Apply a controlled :math:`R_{XX + YY}(\theta, \beta)` gate. + See Also: + :meth:`xx_plus_yy` + Args: theta: The rotation angle beta: The rotation angle control: The control qubit target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`xx_plus_yy` """ def mcxx_plus_yy( self, - theta: float | Expression, - beta: float | Expression, - controls: set[Control | int], + theta: symbolic.Expression | float, + beta: symbolic.Expression | float, + controls: AbstractSet[operations.Control | int], target1: int, target2: int, ) -> None: r"""Apply a multi-controlled :math:`R_{XX + YY}(\theta, \beta)` gate. + See Also: + :meth:`xx_plus_yy` + Args: theta: The rotation angle beta: The rotation angle controls: The control qubits target1: The first target qubit target2: The second target qubit - - See Also: - :meth:`xx_plus_yy` """ - def gphase(self, theta: float) -> None: + def gphase(self, phase: float) -> None: r"""Apply a global phase gate. .. math:: - GPhase(\theta) = (e^{i\theta}) + GPhase(\theta) = (e^{i \theta}) Args: - theta: The rotation angle + phase: The rotation angle """ @overload @@ -1859,11 +1852,8 @@ class QuantumComputation(MutableSequence[Operation]): """Measure all qubits and store the results in classical bits. Details: - If `add_bits` is `True`, a new classical register (named "`meas`") with - the same size as the number of qubits will be added to the circuit - and the results will be stored in it. If `add_bits` is `False`, the - classical register must already exist and have a sufficient number - of bits to store the results. + If `add_bits` is `True`, a new classical register (named "`meas`") with the same size as the number of qubits will be added to the circuit and the results will be stored in it. + If `add_bits` is `False`, the classical register must already exist and have a sufficient number of bits to store the results. Args: add_bits: Whether to explicitly add a classical register @@ -1908,11 +1898,11 @@ class QuantumComputation(MutableSequence[Operation]): @overload def if_else( self, - then_operation: Operation, - else_operation: Operation | None, - control_register: ClassicalRegister, + then_operation: operations.Operation, + else_operation: operations.Operation, + control_register: registers.ClassicalRegister, expected_value: int = 1, - comparison_kind: ComparisonKind = ComparisonKind.eq, + comparison_kind: operations.ComparisonKind = ..., ) -> None: """Add an if-else operation to the circuit. @@ -1927,11 +1917,11 @@ class QuantumComputation(MutableSequence[Operation]): @overload def if_else( self, - then_operation: Operation, - else_operation: Operation | None, + then_operation: operations.Operation, + else_operation: operations.Operation, control_bit: int, expected_value: int = 1, - comparison_kind: ComparisonKind = ComparisonKind.eq, + comparison_kind: operations.ComparisonKind = ..., ) -> None: """Add an if-else operation to the circuit. @@ -1946,14 +1936,14 @@ class QuantumComputation(MutableSequence[Operation]): @overload def if_( self, - op_type: OpType, + op_type: operations.OpType, target: int, - control_register: ClassicalRegister, + control_register: registers.ClassicalRegister, expected_value: int = 1, - comparison_kind: ComparisonKind = ComparisonKind.eq, - params: Sequence[float] = (), + comparison_kind: operations.ComparisonKind = ..., + params: Sequence[float] = ..., ) -> None: - """Add an if operartion to the circuit. + """Add an if operation to the circuit. Args: op_type: The operation to apply @@ -1967,15 +1957,15 @@ class QuantumComputation(MutableSequence[Operation]): @overload def if_( self, - op_type: OpType, + op_type: operations.OpType, target: int, - control: Control | int, - control_register: ClassicalRegister, + control: operations.Control | int, + control_register: registers.ClassicalRegister, expected_value: int = 1, - comparison_kind: ComparisonKind = ComparisonKind.eq, - params: Sequence[float] = (), + comparison_kind: operations.ComparisonKind = ..., + params: Sequence[symbolic.Expression | float] = ..., ) -> None: - """Add a classic-controlled operation to the circuit. + """Add an if operation to the circuit. Args: op_type: The operation to apply @@ -1990,15 +1980,15 @@ class QuantumComputation(MutableSequence[Operation]): @overload def if_( self, - op_type: OpType, + op_type: operations.OpType, target: int, - controls: set[Control | int], - control_register: ClassicalRegister, + controls: AbstractSet[operations.Control | int], + control_register: registers.ClassicalRegister, expected_value: int = 1, - comparison_kind: ComparisonKind = ComparisonKind.eq, - params: Sequence[float] = (), + comparison_kind: operations.ComparisonKind = ..., + params: Sequence[symbolic.Expression | float] = ..., ) -> None: - """Add a classic-controlled operation to the circuit. + """Add an if operation to the circuit. Args: op_type: The operation to apply @@ -2013,14 +2003,14 @@ class QuantumComputation(MutableSequence[Operation]): @overload def if_( self, - op_type: OpType, + op_type: operations.OpType, target: int, control_bit: int, expected_value: bool = True, - comparison_kind: ComparisonKind = ComparisonKind.eq, - params: Sequence[float] = (), + comparison_kind: operations.ComparisonKind = ..., + params: Sequence[float] = ..., ) -> None: - """Add a classic-controlled operation to the circuit. + """Add an if operation to the circuit. Args: op_type: The operation to apply @@ -2034,15 +2024,15 @@ class QuantumComputation(MutableSequence[Operation]): @overload def if_( self, - op_type: OpType, + op_type: operations.OpType, target: int, - control: Control | int, + control: operations.Control | int, control_bit: int, expected_value: bool = True, - comparison_kind: ComparisonKind = ComparisonKind.eq, - params: Sequence[float] = (), + comparison_kind: operations.ComparisonKind = ..., + params: Sequence[symbolic.Expression | float] = ..., ) -> None: - """Add a classic-controlled operation to the circuit. + """Add an if operation to the circuit. Args: op_type: The operation to apply @@ -2057,15 +2047,15 @@ class QuantumComputation(MutableSequence[Operation]): @overload def if_( self, - op_type: OpType, + op_type: operations.OpType, target: int, - controls: set[Control | int], + controls: AbstractSet[operations.Control | int], control_bit: int, expected_value: bool = True, - comparison_kind: ComparisonKind = ComparisonKind.eq, - params: Sequence[float] = (), + comparison_kind: operations.ComparisonKind = ..., + params: Sequence[symbolic.Expression | float] = ..., ) -> None: - """Add a classic-controlled operation to the circuit. + """Add an if operation to the circuit. Args: op_type: The operation to apply diff --git a/python/mqt/core/ir/operations.pyi b/python/mqt/core/ir/operations.pyi index 8fdfe614e3..8701363a7d 100644 --- a/python/mqt/core/ir/operations.pyi +++ b/python/mqt/core/ir/operations.pyi @@ -6,396 +6,409 @@ # # Licensed under the MIT License -from abc import ABC, abstractmethod +import enum from collections.abc import Iterable, Mapping, MutableSequence, Sequence -from enum import Enum +from collections.abc import Set as AbstractSet from typing import overload -from .registers import ClassicalRegister -from .symbolic import Expression, Variable - -__all__ = [ - "ComparisonKind", - "CompoundOperation", - "Control", - "IfElseOperation", - "NonUnitaryOperation", - "OpType", - "Operation", - "StandardOperation", - "SymbolicOperation", -] +import mqt.core.ir.registers +import mqt.core.ir.symbolic -class Control: - """A control is a pair of a qubit and a type. The type can be either positive or negative. +class OpType(enum.Enum): + """Enumeration of operation types.""" - Args: - qubit: The qubit that is the control. - type_: The type of the control (default is positive). + none = 0 """ + A placeholder operation. - class Type(Enum): - """Enumeration of control types. - - It can be either positive or negative. - """ - - Neg = ... - r"""A negative control. - - The operation that is controlled on this qubit is only executed if the qubit is in the :math:`|0\rangle` state. - """ - Pos = ... - r"""A positive control. - - The operation that is controlled on this qubit is only executed if the qubit is in the :math:`|1\rangle` state. - """ - - qubit: int - type_: Type - - def __init__(self, qubit: int, type_: Type = ...) -> None: - """Initialize a control. - - Args: - qubit: The qubit that is the control. - type_: The type of the control (default is positive). - """ - - def __eq__(self, other: object) -> bool: - """Check if two controls are equal.""" - - def __ne__(self, other: object) -> bool: - """Check if two controls are not equal.""" - - def __hash__(self) -> int: - """Get the hash of the control.""" - -class OpType(Enum): - """Enumeration of operation types.""" + It is used to represent an operation that is not yet defined. + """ - barrier = ... + gphase = 4 """ - A barrier operation. It is used to separate operations in the circuit. + A global phase operation. See Also: - :meth:`mqt.core.ir.QuantumComputation.barrier` - """ - if_else = ... + :meth:`mqt.core.ir.QuantumComputation.gphase` """ - An if-else operation. - Used to control the execution of an operation based on the value of a classical register. + i = 10 + """ + An identity operation. See Also: - :meth:`mqt.core.ir.QuantumComputation.if_else` + :meth:`mqt.core.ir.QuantumComputation.i` """ - compound = ... + + h = 16 """ - A compound operation. It is used to group multiple operations into a single operation. + A Hadamard gate. See Also: - :class:`.CompoundOperation` + :meth:`mqt.core.ir.QuantumComputation.h` """ - dcx = ... + + x = 20 """ - A DCX gate. + An X gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.dcx` + :meth:`mqt.core.ir.QuantumComputation.x` """ - ecr = ... + + y = 24 """ - An ECR gate. + A Y gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.ecr` + :meth:`mqt.core.ir.QuantumComputation.y` """ - gphase = ... + + z = 30 """ - A global phase operation. + A Z gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.gphase` + :meth:`mqt.core.ir.QuantumComputation.z` """ - h = ... + + s = 34 """ - A Hadamard gate. + An S gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.h` - """ - i = ... + :meth:`mqt.core.ir.QuantumComputation.s` """ - An identity operation. + + sdg = 35 + r""" + An :math:`S^\dagger` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.i` + :meth:`mqt.core.ir.QuantumComputation.sdg` """ - iswap = ... + + t = 38 """ - An iSWAP gate. + A T gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.iswap` + :meth:`mqt.core.ir.QuantumComputation.t` """ - iswapdg = ... + + tdg = 39 r""" - An :math:`i\text{SWAP}^\dagger` gate. + A :math:`T^\dagger` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.iswapdg` + :meth:`mqt.core.ir.QuantumComputation.tdg` """ - measure = ... + + v = 40 """ - A measurement operation. + A V gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.measure` - """ - none = ... + :meth:`mqt.core.ir.QuantumComputation.v` """ - A placeholder operation. It is used to represent an operation that is not yet defined. + + vdg = 41 + r""" + A :math:`V^\dagger` gate. + + See Also: + :meth:`mqt.core.ir.QuantumComputation.vdg` """ - peres = ... + + u = 44 """ - A Peres gate. + A U gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.peres` + :meth:`mqt.core.ir.QuantumComputation.u` """ - peresdg = ... - r""" - A :math:`\text{Peres}^\dagger` gate. + + u2 = 48 + """ + A U2 gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.peresdg` + :meth:`mqt.core.ir.QuantumComputation.u2` """ - p = ... + + p = 54 """ A phase gate. See Also: :meth:`mqt.core.ir.QuantumComputation.p` """ - reset = ... - """ - A reset operation. + + sx = 56 + r""" + A :math:`\sqrt{X}` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.reset` + :meth:`mqt.core.ir.QuantumComputation.sx` """ - r = ... + + sxdg = 57 r""" - An :math:`R` gate. + A :math:`\sqrt{X}^\dagger` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.r` + :meth:`mqt.core.ir.QuantumComputation.sxdg` """ - rx = ... - r""" - An :math:`R_x` gate. + + rx = 60 + """ + A :math:`R_x` gate. See Also: :meth:`mqt.core.ir.QuantumComputation.rx` """ - rxx = ... - r""" - An :math:`R_{xx}` gate. - See Also: - :meth:`mqt.core.ir.QuantumComputation.rxx` + ry = 64 """ - ry = ... - r""" - An :math:`R_y` gate. + A :math:`R_y` gate. See Also: :meth:`mqt.core.ir.QuantumComputation.ry` """ - ryy = ... - r""" - An :math:`R_{yy}` gate. - See Also: - :meth:`mqt.core.ir.QuantumComputation.ryy` + rz = 70 """ - rz = ... - r""" - An :math:`R_z` gate. + A :math:`R_z` gate. See Also: :meth:`mqt.core.ir.QuantumComputation.rz` """ - rzx = ... - r""" - An :math:`R_{zx}` gate. + + r = 164 + """ + An :math:`R` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.rzx` + :meth:`mqt.core.ir.QuantumComputation.r` """ - rzz = ... - r""" - An :math:`R_{zz}` gate. + + swap = 72 + """ + A SWAP gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.rzz` + :meth:`mqt.core.ir.QuantumComputation.swap` """ - s = ... + + iswap = 76 """ - An S gate. + A iSWAP gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.s` + :meth:`mqt.core.ir.QuantumComputation.iswap` """ - sdg = ... + + iswapdg = 77 r""" - An :math:`S^\dagger` gate. + A :math:`i\text{SWAP}^\dagger` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.sdg` - """ - swap = ... + :meth:`mqt.core.ir.QuantumComputation.iswapdg` """ - A SWAP gate. - See Also: - :meth:`mqt.core.ir.QuantumComputation.swap` + peres = 80 """ - sx = ... - r""" - A :math:`\sqrt{X}` gate. + A Peres gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.sx` + :meth:`mqt.core.ir.QuantumComputation.peres` """ - sxdg = ... + + peresdg = 81 r""" - A :math:`\sqrt{X}^\dagger` gate. + A :math:`\text{Peres}^\dagger` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.sxdg` - """ - t = ... + :meth:`mqt.core.ir.QuantumComputation.peresdg` """ - A T gate. - See Also: - :meth:`mqt.core.ir.QuantumComputation.t` + dcx = 84 """ - tdg = ... - r""" - A :math:`T^\dagger` gate. + A DCX gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.tdg` + :meth:`mqt.core.ir.QuantumComputation.dcx` """ - u2 = ... + + ecr = 88 """ - A U2 gate. + An ECR gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.u2` + :meth:`mqt.core.ir.QuantumComputation.ecr` """ - u = ... + + rxx = 92 """ - A U gate. + A :math:`R_{xx}` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.u` + :meth:`mqt.core.ir.QuantumComputation.rxx` """ - v = ... + + ryy = 96 """ - A V gate. + A :math:`R_{yy}` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.v` + :meth:`mqt.core.ir.QuantumComputation.ryy` """ - vdg = ... - r""" - A :math:`V^\dagger` gate. + + rzz = 102 + """ + A :math:`R_{zz}` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.vdg` + :meth:`mqt.core.ir.QuantumComputation.rzz` """ - x = ... + + rzx = 104 """ - An X gate. + A :math:`R_{zx}` gate. See Also: - :meth:`mqt.core.ir.QuantumComputation.x` + :meth:`mqt.core.ir.QuantumComputation.rzx` """ - xx_minus_yy = ... - r""" - An :math:`R_{XX - YY}` gate. + + xx_minus_yy = 108 + """ + A :math:`R_{XX - YY}` gate. See Also: :meth:`mqt.core.ir.QuantumComputation.xx_minus_yy` """ - xx_plus_yy = ... - r""" - An :math:`R_{XX + YY}` gate. + + xx_plus_yy = 112 + """ + A :math:`R_{XX + YY}` gate. See Also: :meth:`mqt.core.ir.QuantumComputation.xx_plus_yy` """ - y = ... + + compound = 116 """ - A Y gate. + A compound operation. - See Also: - :meth:`mqt.core.ir.QuantumComputation.y` + It is used to group multiple operations into a single operation. + + See also :class:`.CompoundOperation` """ - z = ... + + measure = 120 """ - A Z gate. + A measurement operation. See Also: - :meth:`mqt.core.ir.QuantumComputation.z` + :meth:`mqt.core.ir.QuantumComputation.measure` """ -class Operation(ABC): - """An abstract base class for operations that can be added to a :class:`~mqt.core.ir.QuantumComputation`.""" - - type_: OpType - """ - The type of the operation. + reset = 124 """ - controls: set[Control] + A reset operation. + + See Also: + :meth:`mqt.core.ir.QuantumComputation.reset` """ - The controls of the operation. - Note: - The notion of a control might not make sense for all types of operations. + barrier = 14 """ - targets: list[int] + A barrier operation. + + It is used to separate operations in the circuit. + + See Also: + :meth:`mqt.core.ir.QuantumComputation.barrier` """ - The targets of the operation. - Note: - The notion of a target might not make sense for all types of operations. + if_else = 128 """ - parameter: list[float] + An if-else operation. + + It is used to control the execution of an operation based on the value of a classical register. + + See Also: + :meth:`mqt.core.ir.QuantumComputation.if_else` """ - The parameters of the operation. - Note: - The notion of a parameter might not make sense for all types of operations. +class Control: + """A control is a pair of a qubit and a type. The type can be either positive or negative. + + Args: + qubit: The qubit that is the control. + type_: The type of the control. """ + def __init__(self, qubit: int, type_: Control.Type = ...) -> None: ... + + class Type(enum.Enum): + """Enumeration of control types.""" + + Pos = 1 + + Neg = 0 + + @property + def qubit(self) -> int: + """The qubit that is the control.""" + + @property + def type_(self) -> Control.Type: + """The type of the control.""" + + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + def __hash__(self) -> int: ... + +class Operation: @property def name(self) -> str: """The name of the operation.""" + @property + def type_(self) -> OpType: + """The type of the operation.""" + + @type_.setter + def type_(self, arg: OpType, /) -> None: ... + @property + def targets(self) -> list[int]: + """The targets of the operation. + + Note: + The notion of a target might not make sense for all types of operations. + """ + + @targets.setter + def targets(self, arg: Sequence[int], /) -> None: ... @property def num_targets(self) -> int: """The number of targets of the operation.""" + @property + def controls(self) -> set[Control]: + """The controls of the operation. + + Note: + The notion of a control might not make sense for all types of operations. + """ + + @controls.setter + def controls(self, arg: AbstractSet[Control], /) -> None: ... @property def num_controls(self) -> int: """The number of controls of the operation.""" - @abstractmethod def add_control(self, control: Control) -> None: """Add a control to the operation. @@ -403,18 +416,16 @@ class Operation(ABC): control: The control to add. """ - def add_controls(self, controls: set[Control]) -> None: + def add_controls(self, controls: AbstractSet[Control]) -> None: """Add multiple controls to the operation. Args: controls: The controls to add. """ - @abstractmethod def clear_controls(self) -> None: """Clear all controls of the operation.""" - @abstractmethod def remove_control(self, control: Control) -> None: """Remove a control from the operation. @@ -422,13 +433,20 @@ class Operation(ABC): control: The control to remove. """ - def remove_controls(self, controls: set[Control]) -> None: + def remove_controls(self, controls: AbstractSet[Control]) -> None: """Remove multiple controls from the operation. Args: controls: The controls to remove. """ + def get_used_qubits(self) -> set[int]: + """Get the qubits that are used by the operation. + + Returns: + The set of qubits that are used by the operation. + """ + def acts_on(self, qubit: int) -> bool: """Check if the operation acts on a specific qubit. @@ -439,32 +457,35 @@ class Operation(ABC): True if the operation acts on the qubit, False otherwise. """ - def get_used_qubits(self) -> set[int]: - """Get the qubits that are used by the operation. + @property + def parameter(self) -> list[float]: + """The parameters of the operation. - Returns: - The set of qubits that are used by the operation. + Note: + The notion of a parameter might not make sense for all types of operations. """ - def is_if_else_operation(self) -> bool: - """Check if the operation is a :class:`IfElseOperation`. + @parameter.setter + def parameter(self, arg: Sequence[float], /) -> None: ... + def is_unitary(self) -> bool: + """Check if the operation is unitary. Returns: - True if the operation is a :class:`IfElseOperation`, False otherwise. + True if the operation is unitary, False otherwise. """ - def is_compound_operation(self) -> bool: - """Check if the operation is a :class:`CompoundOperation`. + def is_standard_operation(self) -> bool: + """Check if the operation is a :class:`StandardOperation`. Returns: - True if the operation is a :class:`CompoundOperation`, False otherwise. + True if the operation is a :class:`StandardOperation`, False otherwise. """ - def is_controlled(self) -> bool: - """Check if the operation is controlled. + def is_compound_operation(self) -> bool: + """Check if the operation is a :class:`CompoundOperation`. Returns: - True if the operation is controlled, False otherwise. + True if the operation is a :class:`CompoundOperation`, False otherwise. """ def is_non_unitary_operation(self) -> bool: @@ -474,11 +495,11 @@ class Operation(ABC): True if the operation is a :class:`NonUnitaryOperation`, False otherwise. """ - def is_standard_operation(self) -> bool: - """Check if the operation is a :class:`StandardOperation`. + def is_if_else_operation(self) -> bool: + """Check if the operation is a :class:`IfElseOperation`. Returns: - True if the operation is a :class:`StandardOperation`, False otherwise. + True if the operation is a :class:`IfElseOperation`, False otherwise. """ def is_symbolic_operation(self) -> bool: @@ -488,11 +509,11 @@ class Operation(ABC): True if the operation is a :class:`SymbolicOperation`, False otherwise. """ - def is_unitary(self) -> bool: - """Check if the operation is unitary. + def is_controlled(self) -> bool: + """Check if the operation is controlled. Returns: - True if the operation is unitary, False otherwise. + True if the operation is controlled, False otherwise. """ def get_inverted(self) -> Operation: @@ -502,21 +523,19 @@ class Operation(ABC): The inverse of the operation. """ - @abstractmethod def invert(self) -> None: """Invert the operation (in-place).""" - def __eq__(self, other: object) -> bool: ... + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... def __hash__(self) -> int: ... - def __ne__(self, other: object) -> bool: ... class StandardOperation(Operation): """Standard quantum operation. - This class is used to represent all standard quantum operations, i.e., - operations that are unitary. This includes all possible quantum gates. - Such Operations are defined by their :class:`OpType`, the qubits (controls - and targets) they act on, and their parameters. + This class is used to represent all standard quantum operations, i.e., operations that are unitary. + This includes all possible quantum gates. + Such Operations are defined by their :class:`OpType`, the qubits (controls and targets) they act on, and their parameters. Args: control: The control qubit(s) of the operation (if any). @@ -528,127 +547,34 @@ class StandardOperation(Operation): @overload def __init__(self) -> None: ... @overload - def __init__( - self, - target: int, - op_type: OpType, - params: Sequence[float] | None = None, - ) -> None: ... + def __init__(self, target: int, op_type: OpType, params: Sequence[float] = ...) -> None: ... @overload - def __init__( - self, - targets: Sequence[int], - op_type: OpType, - params: Sequence[float] | None = None, - ) -> None: ... + def __init__(self, targets: Sequence[int], op_type: OpType, params: Sequence[float] = ...) -> None: ... @overload - def __init__( - self, - control: Control, - target: int, - op_type: OpType, - params: Sequence[float] | None = None, - ) -> None: ... + def __init__(self, control: Control, target: int, op_type: OpType, params: Sequence[float] = ...) -> None: ... @overload def __init__( - self, - control: Control, - targets: Sequence[int], - op_type: OpType, - params: Sequence[float] | None = None, + self, control: Control, targets: Sequence[int], op_type: OpType, params: Sequence[float] = ... ) -> None: ... @overload def __init__( - self, - controls: set[Control], - target: int, - op_type: OpType, - params: Sequence[float] | None = None, + self, controls: AbstractSet[Control], target: int, op_type: OpType, params: Sequence[float] = ... ) -> None: ... @overload def __init__( - self, - controls: set[Control], - targets: Sequence[int], - op_type: OpType, - params: Sequence[float] | None = None, + self, controls: AbstractSet[Control], targets: Sequence[int], op_type: OpType, params: Sequence[float] = ... ) -> None: ... @overload def __init__( - self, - controls: set[Control], - target0: int, - target1: int, - op_type: OpType, - params: Sequence[float] | None = None, + self, controls: AbstractSet[Control], target0: int, target1: int, op_type: OpType, params: Sequence[float] = ... ) -> None: ... - def add_control(self, control: Control) -> None: - """Add a control to the operation. - - :class:`StandardOperation` supports arbitrarily many controls per operation. - - Args: - control: The control to add. - """ - - def clear_controls(self) -> None: - """Clear all controls of the operation.""" - - def remove_control(self, control: Control) -> None: - """Remove a control from the operation. - - Args: - control: The control to remove. - """ - - def invert(self) -> None: - """Invert the operation (in-place). - - Since any :class:`StandardOperation` is unitary, the inverse is simply the - conjugate transpose of the operation's matrix representation. - """ - -class NonUnitaryOperation(Operation): - """Non-unitary operation. - - This class is used to represent all non-unitary operations, i.e., operations - that are not reversible. This includes measurements and resets. - - Args: - targets: The target qubit(s) of the operation. - classics: The classical bit(s) that are associated with the operation (only relevant for measurements). - op_type: The type of the operation. - """ - - @property - def classics(self) -> list[int]: - """The classical registers that are associated with the operation.""" - - @overload - def __init__(self, targets: Sequence[int], classics: Sequence[int]) -> None: ... - @overload - def __init__(self, target: int, classic: int) -> None: ... - @overload - def __init__(self, targets: Sequence[int], op_type: OpType = ...) -> None: ... - def add_control(self, control: Control) -> None: - """Adding controls to a non-unitary operation is not supported.""" - - def clear_controls(self) -> None: - """Cannot clear controls of a non-unitary operation.""" - - def remove_control(self, control: Control) -> None: - """Removing controls from a non-unitary operation is not supported.""" - - def invert(self) -> None: - """Non-unitary operations are, per definition, not invertible.""" class CompoundOperation(Operation, MutableSequence[Operation]): """Compound quantum operation. - This class is used to aggregate and group multiple operations into a single - object. This is useful for optimizations and for representing complex - quantum functionality. A :class:`CompoundOperation` can contain any number - of operations, including other :class:`CompoundOperation`'s. + This class is used to aggregate and group multiple operations into a single object. + This is useful for optimizations and for representing complex quantum functionality. + A :class:`CompoundOperation` can contain any number of operations, including other :class:`CompoundOperation`'s. Args: ops: The operations that are part of the compound operation. @@ -665,28 +591,28 @@ class CompoundOperation(Operation, MutableSequence[Operation]): def __getitem__(self, index: int) -> Operation: """Get the operation at the given index. + Note: + This gives direct access to the operations in the compound operation + Args: index: The index of the operation to get. Returns: The operation at the given index. - - Notes: - This gives direct access to the operations in the compound operation. """ @overload def __getitem__(self, index: slice) -> list[Operation]: """Get the operations in the given slice. + Note: + This gives direct access to the operations in the compound operation. + Args: index: The slice of the operations to get. Returns: The operations in the given slice. - - Notes: - This gives direct access to the operations in the compound operation. """ @overload @@ -723,6 +649,9 @@ class CompoundOperation(Operation, MutableSequence[Operation]): index: The slice of operations to delete. """ + def append(self, value: Operation) -> None: + """Append an operation to the compound operation.""" + def insert(self, index: int, value: Operation) -> None: """Insert an operation at the given index. @@ -731,85 +660,58 @@ class CompoundOperation(Operation, MutableSequence[Operation]): value: The operation to insert. """ - def append(self, value: Operation) -> None: - """Append an operation to the compound operation.""" - def empty(self) -> bool: """Check if the compound operation is empty.""" def clear(self) -> None: """Clear all operations in the compound operation.""" - def add_control(self, control: Control) -> None: - """Add a control to the operation. - - This will add the control to all operations in the compound operation. - Additionally, the control is added to the compound operation itself to - keep track of all controls that are applied to the compound operation. - - Args: - control: The control to add. - """ - - def clear_controls(self) -> None: - """Clear all controls of the operation. - - This will clear all controls that have been tracked in the compound - operation itself and will clear these controls of all operations that are - part of the compound operation. - """ - - def remove_control(self, control: Control) -> None: - """Remove a control from the operation. +class NonUnitaryOperation(Operation): + """Non-unitary operation. - This will remove the control from all operations in the compound operation. - Additionally, the control is removed from the compound operation itself to - keep track of all controls that are applied to the compound operation. + This class is used to represent all non-unitary operations, i.e., operations that are not reversible. + This includes measurements and resets. - Args: - control: The control to remove. - """ - - def invert(self) -> None: - """Invert the operation (in-place). + Args: + targets: The target qubit(s) of the operation. + classics: The classical bit(s) that are associated with the operation (only relevant for measurements). + op_type: The type of the operation. + """ - This will invert all operations in the compound operation and reverse - the order of the operations. This only works if all operations in the - compound operation are invertible and will throw an error otherwise. - """ + @overload + def __init__(self, targets: Sequence[int], classics: Sequence[int]) -> None: ... + @overload + def __init__(self, target: int, classic: int) -> None: ... + @overload + def __init__(self, targets: Sequence[int], op_type: OpType = ...) -> None: ... + @property + def classics(self) -> list[int]: + """The classical bits that are associated with the operation.""" class SymbolicOperation(StandardOperation): """Symbolic quantum operation. - This class is used to represent quantum operations that are not yet fully - defined. This can be useful for representing operations that depend on - parameters that are not yet known. A :class:`SymbolicOperation` is defined - by its :class:`OpType`, the qubits (controls and targets) it acts on, and - its parameters. The parameters can be either fixed values or symbolic - expressions. + This class is used to represent quantum operations that are not yet fully defined. + This can be useful for representing operations that depend on parameters that are not yet known. + A :class:`SymbolicOperation` is defined by its :class:`OpType`, the qubits (controls and targets) it acts on, and its parameters. + The parameters can be either fixed values or symbolic expressions. Args: - controls: The control qubit(s) of the operation (if any). - targets: The target qubit(s) of the operation. - op_type: The type of the operation. - params: The parameters of the operation (if any). + controls: The control qubit(s) of the operation (if any). + targets: The target qubit(s) of the operation. + op_type: The type of the operation. + params: The parameters of the operation (if any). """ @overload def __init__(self) -> None: ... @overload def __init__( - self, - target: int, - op_type: OpType, - params: Sequence[Expression | float] | None = None, + self, target: int, op_type: OpType, params: Sequence[mqt.core.ir.symbolic.Expression | float] = ... ) -> None: ... @overload def __init__( - self, - targets: Sequence[int], - op_type: OpType, - params: Sequence[Expression | float] | None = None, + self, targets: Sequence[int], op_type: OpType, params: Sequence[mqt.core.ir.symbolic.Expression | float] = ... ) -> None: ... @overload def __init__( @@ -817,7 +719,7 @@ class SymbolicOperation(StandardOperation): control: Control, target: int, op_type: OpType, - params: Sequence[Expression | float] | None = None, + params: Sequence[mqt.core.ir.symbolic.Expression | float] = ..., ) -> None: ... @overload def __init__( @@ -825,88 +727,96 @@ class SymbolicOperation(StandardOperation): control: Control, targets: Sequence[int], op_type: OpType, - params: Sequence[Expression | float] | None = None, + params: Sequence[mqt.core.ir.symbolic.Expression | float] = ..., ) -> None: ... @overload def __init__( self, - controls: set[Control], + controls: AbstractSet[Control], target: int, op_type: OpType, - params: Sequence[Expression | float] | None = None, + params: Sequence[mqt.core.ir.symbolic.Expression | float] = ..., ) -> None: ... @overload def __init__( self, - controls: set[Control], + controls: AbstractSet[Control], targets: Sequence[int], op_type: OpType, - params: Sequence[Expression | float] | None = None, + params: Sequence[mqt.core.ir.symbolic.Expression | float] = ..., ) -> None: ... @overload def __init__( self, - controls: set[Control], + controls: AbstractSet[Control], target0: int, target1: int, op_type: OpType, - params: Sequence[Expression | float] | None = None, + params: Sequence[mqt.core.ir.symbolic.Expression | float] = ..., ) -> None: ... - def get_parameter(self, idx: int) -> Expression | float: + def get_parameter(self, index: int) -> mqt.core.ir.symbolic.Expression | float: """Get the parameter at the given index. Args: - idx: The index of the parameter to get. + index: The index of the parameter to get. Returns: - The parameter at the given index. + The parameter at the given index. """ - def get_parameters(self) -> list[Expression | float]: + def get_parameters(self) -> list[mqt.core.ir.symbolic.Expression | float]: """Get all parameters of the operation. Returns: - The parameters of the operation. + The parameters of the operation. """ - def get_instantiated_operation(self, assignment: Mapping[Variable, float]) -> StandardOperation: + def get_instantiated_operation( + self, assignment: Mapping[mqt.core.ir.symbolic.Variable, float] + ) -> StandardOperation: """Get the instantiated operation. Args: - assignment: The assignment of the symbolic parameters. + assignment: The assignment of the symbolic parameters. Returns: - The instantiated operation. + The instantiated operation. """ - def instantiate(self, assignment: Mapping[Variable, float]) -> None: + def instantiate(self, assignment: Mapping[mqt.core.ir.symbolic.Variable, float]) -> None: """Instantiate the operation (in-place). Args: - assignment: The assignment of the symbolic parameters. + assignment: The assignment of the symbolic parameters. """ -class ComparisonKind(Enum): +class ComparisonKind(enum.Enum): """Enumeration of comparison types for classic-controlled operations.""" - eq = ... + eq = 0 """Equality comparison.""" - neq = ... + + neq = 1 """Inequality comparison.""" - lt = ... - """Less than comparison.""" - leq = ... - """Less than or equal comparison.""" - gt = ... - """Greater than comparison.""" - geq = ... - """Greater than or equal comparison.""" + + lt = 2 + """Less-than comparison.""" + + leq = 3 + """Less-than-or-equal comparison.""" + + gt = 4 + """Greater-than comparison.""" + + geq = 5 + """Greater-than-or-equal comparison.""" class IfElseOperation(Operation): """If-else quantum operation. - This class is used to represent an if-else operation. The then operation is executed if the - value of the classical register matches the expected value. Otherwise, the else operation is executed. + This class is used to represent an if-else operation. + The then operation is executed if the value of the classical register matches the expected value. + Otherwise, the else operation is executed. Args: then_operation: The operation that is executed if the condition is met. @@ -919,16 +829,16 @@ class IfElseOperation(Operation): @overload def __init__( self, - if_operation: Operation, + then_operation: Operation, else_operation: Operation | None, - control_register: ClassicalRegister, + control_register: mqt.core.ir.registers.ClassicalRegister, expected_value: int = 1, comparison_kind: ComparisonKind = ..., ) -> None: ... @overload def __init__( self, - if_operation: Operation, + then_operation: Operation, else_operation: Operation | None, control_bit: int, expected_value: bool = True, @@ -943,7 +853,7 @@ class IfElseOperation(Operation): """The operation that is executed if the condition is not met.""" @property - def control_register(self) -> ClassicalRegister | None: + def control_register(self) -> mqt.core.ir.registers.ClassicalRegister | None: """The classical register that controls the operation.""" @property @@ -954,58 +864,20 @@ class IfElseOperation(Operation): def expected_value_register(self) -> int: """The expected value of the classical register. - The then-operation is executed if the value of the classical register matches the expected value based on the - kind of comparison. - The expected value is an integer that is interpreted as a binary number, where the least significant bit is at - the start index of the classical register. + The then-operation is executed if the value of the classical register matches the expected value based on the kind of comparison. + The expected value is an integer that is interpreted as a binary number, where the least significant bit is at the start index of the classical register. """ @property def expected_value_bit(self) -> bool: - """The expected value of the classical register. + """The expected value of the classical bit. - The then-operation is executed if the value of the classical bit matches the expected value based on the - kind of comparison. + The then-operation is executed if the value of the classical bit matches the expected value based on the kind of comparison. """ @property def comparison_kind(self) -> ComparisonKind: """The kind of comparison. - The then-operation is executed if the value of the classical register matches the expected value based on the - kind of comparison. - """ - - def add_control(self, control: Control) -> None: - """Adds a control to the underlying operation. - - Args: - control: The control to add. - - See Also: - :meth:`Operation.add_control` - """ - - def clear_controls(self) -> None: - """Clears the controls of the underlying operation. - - See Also: - :meth:`Operation.clear_controls` - """ - - def remove_control(self, control: Control) -> None: - """Removes a control from the underlying operation. - - Args: - control: The control to remove. - - See Also: - :meth:`Operation.remove_control` - """ - - def invert(self) -> None: - """Inverts the underlying operation. - - See Also: - :meth:`Operation.invert` + The then-operation is executed if the value of the control matches the expected value based on the kind of comparison. """ diff --git a/python/mqt/core/ir/registers.pyi b/python/mqt/core/ir/registers.pyi index c9f1be9969..a224a4a7ef 100644 --- a/python/mqt/core/ir/registers.pyi +++ b/python/mqt/core/ir/registers.pyi @@ -6,8 +6,6 @@ # # Licensed under the MIT License -__all__ = ["ClassicalRegister", "QuantumRegister"] - class QuantumRegister: """A class to represent a collection of qubits. @@ -19,34 +17,32 @@ class QuantumRegister: def __init__(self, start: int, size: int, name: str = "") -> None: ... @property - def start(self) -> int: - """The index of the first qubit in the quantum register.""" + def name(self) -> str: + """The name of the quantum register.""" @property - def end(self) -> int: - """Index of the last qubit in the quantum register.""" + def start(self) -> int: + """The index of the first qubit in the quantum register.""" + @start.setter + def start(self, arg: int, /) -> None: ... @property def size(self) -> int: """The number of qubits in the quantum register.""" + @size.setter + def size(self, arg: int, /) -> None: ... @property - def name(self) -> str: - """The name of the quantum register.""" - - def __eq__(self, other: object) -> bool: - """Check if the quantum register is equal to another quantum register.""" - - def __ne__(self, other: object) -> bool: - """Check if the quantum register is not equal to another quantum register.""" - - def __hash__(self) -> int: - """Return the hash of the quantum register.""" + def end(self) -> int: + """Index of the last qubit in the quantum register.""" + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + def __hash__(self) -> int: ... def __getitem__(self, key: int) -> int: """Get the qubit at the specified index.""" - def __contains__(self, qubit: int) -> bool: + def __contains__(self, item: int) -> bool: """Check if the quantum register contains a qubit.""" class ClassicalRegister: @@ -60,32 +56,30 @@ class ClassicalRegister: def __init__(self, start: int, size: int, name: str = "") -> None: ... @property - def start(self) -> int: - """The index of the first bit in the classical register.""" + def name(self) -> str: + """The name of the classical register.""" @property - def end(self) -> int: - """Index of the last bit in the classical register.""" + def start(self) -> int: + """The index of the first bit in the classical register.""" + @start.setter + def start(self, arg: int, /) -> None: ... @property def size(self) -> int: """The number of bits in the classical register.""" + @size.setter + def size(self, arg: int, /) -> None: ... @property - def name(self) -> str: - """The name of the classical register.""" - - def __eq__(self, other: object) -> bool: - """Check if the classical register is equal to another classical register.""" - - def __ne__(self, other: object) -> bool: - """Check if the classical register is not equal to another classical register.""" - - def __hash__(self) -> int: - """Return the hash of the classical register.""" + def end(self) -> int: + """Index of the last bit in the classical register.""" + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + def __hash__(self) -> int: ... def __getitem__(self, key: int) -> int: """Get the bit at the specified index.""" - def __contains__(self, bit: int) -> bool: + def __contains__(self, item: int) -> bool: """Check if the classical register contains a bit.""" diff --git a/python/mqt/core/ir/symbolic.pyi b/python/mqt/core/ir/symbolic.pyi index 2c1cd34b86..e58b4b8f09 100644 --- a/python/mqt/core/ir/symbolic.pyi +++ b/python/mqt/core/ir/symbolic.pyi @@ -9,29 +9,27 @@ from collections.abc import Iterator, Mapping, Sequence from typing import overload -__all__ = ["Expression", "Term", "Variable"] - class Variable: """A symbolic variable. + Note: + Variables are uniquely identified by their name, so if a variable with the same name already exists, the existing variable will be returned. + Args: name: The name of the variable. - - Note: - Variables are uniquely identified by their name, so if a variable with the same name already exists, - the existing variable will be returned. """ - def __eq__(self, arg0: object) -> bool: ... - def __gt__(self, arg0: Variable) -> bool: ... - def __hash__(self) -> int: ... def __init__(self, name: str = "") -> None: ... - def __lt__(self, arg0: Variable) -> bool: ... - def __ne__(self, arg0: object) -> bool: ... @property def name(self) -> str: """The name of the variable.""" + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + def __hash__(self) -> int: ... + def __lt__(self, arg: Variable, /) -> bool: ... + def __gt__(self, arg: Variable, /) -> bool: ... + class Term: """A symbolic term which consists of a variable with a given coefficient. @@ -40,14 +38,18 @@ class Term: coefficient: The coefficient of the term. """ - def __eq__(self, arg0: object) -> bool: ... - def __hash__(self) -> int: ... def __init__(self, variable: Variable, coefficient: float = 1.0) -> None: ... - def __mul__(self, arg0: float) -> Term: ... - def __ne__(self, arg0: object) -> bool: ... - def __rmul__(self, arg0: float) -> Term: ... - def __rtruediv__(self, arg0: float) -> Term: ... - def __truediv__(self, arg0: float) -> Term: ... + @property + def variable(self) -> Variable: + """The variable of the term.""" + + @property + def coefficient(self) -> float: + """The coefficient of the term.""" + + def has_zero_coefficient(self) -> bool: + """Check if the coefficient of the term is zero.""" + def add_coefficient(self, coeff: float) -> None: """Add a coefficient to the coefficient of this term. @@ -65,109 +67,48 @@ class Term: The evaluated value of the term. """ - def has_zero_coefficient(self) -> bool: - """Check if the coefficient of the term is zero.""" - - @property - def coefficient(self) -> float: - """The coefficient of the term.""" - - @property - def variable(self) -> Variable: - """The variable of the term.""" + def __mul__(self, arg: float, /) -> Term: ... + def __rmul__(self, arg: float, /) -> Term: ... + def __truediv__(self, arg: float, /) -> Term: ... + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + def __hash__(self) -> int: ... class Expression: r"""A symbolic expression which consists of a sum of terms and a constant. The expression is of the form :math:`constant + term_1 + term_2 + \dots + term_n`. + Alternatively, an expression can be created with a single term and a constant or just a constant. Args: terms: The list of terms. constant: The constant. - - Alternatively, an expression can be created with a single term and a constant or just a constant. - """ - - constant: float - """ - The constant of the expression. """ @overload - def __add__(self, arg0: Expression) -> Expression: ... + def __init__(self, constant: float = 0.0) -> None: ... @overload - def __add__(self, arg0: Term) -> Expression: ... - @overload - def __add__(self, arg0: float) -> Expression: ... - def __eq__(self, arg0: object) -> bool: ... - def __getitem__(self, idx: int) -> Term: ... - def __hash__(self) -> int: ... - @overload - def __init__(self, terms: Sequence[Term], constant: float = 0.0) -> None: - """Create an expression with a given list of terms and a constant. - - Args: - terms: The list of terms. - constant: The constant. - """ - - @overload - def __init__(self, term: Term, constant: float = 0.0) -> None: - """Create an expression with a given term and a constant. - - Args: - term: The term. - constant: The constant. - """ - + def __init__(self, terms: Sequence[Term], constant: float = 0.0) -> None: ... @overload - def __init__(self, constant: float = 0.0) -> None: - """Create an expression with a given constant. - - Args: - constant: The constant. - """ + def __init__(self, term: Term, constant: float = 0.0) -> None: ... + @property + def constant(self) -> float: + """The constant of the expression.""" + @constant.setter + def constant(self, arg: float, /) -> None: ... def __iter__(self) -> Iterator[Term]: ... - def __len__(self) -> int: ... - def __mul__(self, arg0: float) -> Expression: ... - def __ne__(self, arg0: object) -> bool: ... - @overload - def __radd__(self, arg0: Term) -> Expression: ... - @overload - def __radd__(self, arg0: float) -> Expression: ... - def __rmul__(self, arg0: float) -> Expression: ... - @overload - def __rsub__(self, arg0: Term) -> Expression: ... - @overload - def __rsub__(self, arg0: float) -> Expression: ... - def __rtruediv__(self, arg0: float) -> Expression: ... - @overload - def __sub__(self, arg0: Expression) -> Expression: ... - @overload - def __sub__(self, arg0: Term) -> Expression: ... - @overload - def __sub__(self, arg0: float) -> Expression: ... - def __truediv__(self, arg0: float) -> Expression: ... - def evaluate(self, assignment: Mapping[Variable, float]) -> float: - """Evaluate the expression with a given variable assignment. - - Args: - assignment: The variable assignment. - - Returns: - The evaluated value of the expression. - """ + def __getitem__(self, index: int) -> Term: ... + def is_zero(self) -> bool: + """Check if the expression is zero.""" def is_constant(self) -> bool: """Check if the expression is a constant.""" - def is_zero(self) -> bool: - """Check if the expression is zero.""" - def num_terms(self) -> int: """The number of terms in the expression.""" + def __len__(self) -> int: ... @property def terms(self) -> list[Term]: """The terms of the expression.""" @@ -175,3 +116,40 @@ class Expression: @property def variables(self) -> set[Variable]: """The variables in the expression.""" + + def evaluate(self, assignment: Mapping[Variable, float]) -> float: + """Evaluate the expression with a given variable assignment. + + Args: + assignment: The variable assignment. + + Returns: + The evaluated value of the expression. + """ + + @overload + def __add__(self, arg: Expression, /) -> Expression: ... + @overload + def __add__(self, arg: float, /) -> Expression: ... + @overload + def __add__(self, arg: Term, /) -> Expression: ... + @overload + def __radd__(self, arg: Term, /) -> Expression: ... + @overload + def __radd__(self, arg: float, /) -> Expression: ... + @overload + def __sub__(self, arg: Expression, /) -> Expression: ... + @overload + def __sub__(self, arg: float, /) -> Expression: ... + @overload + def __sub__(self, arg: Term, /) -> Expression: ... + @overload + def __rsub__(self, arg: float, /) -> Expression: ... + @overload + def __rsub__(self, arg: Term, /) -> Expression: ... + def __mul__(self, arg: float, /) -> Expression: ... + def __rmul__(self, arg: float, /) -> Expression: ... + def __truediv__(self, arg: float, /) -> Expression: ... + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + def __hash__(self) -> int: ... diff --git a/python/mqt/core/na/__init__.py b/python/mqt/core/na/__init__.pyi similarity index 62% rename from python/mqt/core/na/__init__.py rename to python/mqt/core/na/__init__.pyi index 4f663baa34..e89adef973 100644 --- a/python/mqt/core/na/__init__.py +++ b/python/mqt/core/na/__init__.pyi @@ -6,7 +6,4 @@ # # Licensed under the MIT License -"""MQT Core NA - The MQT Core neutral atom module. - -This module contains all neutral atom related functionality of MQT Core. -""" +from . import fomac as fomac diff --git a/python/mqt/core/na/fomac.pyi b/python/mqt/core/na/fomac.pyi index 4757aa46e4..458bc5bdbc 100644 --- a/python/mqt/core/na/fomac.pyi +++ b/python/mqt/core/na/fomac.pyi @@ -6,102 +6,93 @@ # # Licensed under the MIT License -"""Reconstruction of NADevice from QDMI's Device class.""" +import mqt.core.fomac -from collections.abc import Iterable +class Device(mqt.core.fomac.Device): + """Represents a device with a lattice of traps.""" -from ..fomac import Device as GenericDevice + class Lattice: + """Represents a lattice of traps in the device.""" -__all__ = ["Device"] + class Vector: + """Represents a 2D vector.""" -class Device(GenericDevice): - """Represents a device with a lattice of traps.""" + @property + def x(self) -> int: + """The x-coordinate of the vector.""" - class Vector: - """Represents a 2D vector.""" + @property + def y(self) -> int: + """The y-coordinate of the vector.""" - x: int - """ - The x-coordinate of the vector. - """ - y: int - """ - The y-coordinate of the vector. - """ - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... - class Region: - """Represents a region in the device.""" + class Region: + """Represents a region in the device.""" - origin: Device.Vector - """ - The origin of the region. - """ + class Size: + """Represents the size of a region.""" - class Size: - """Represents the size of a region.""" - - width: int - """ - The width of the region. - """ - height: int - """ - The height of the region. - """ - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - - size: Size - """ - The size of the region. - """ + @property + def width(self) -> int: + """The width of the region.""" - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... + @property + def height(self) -> int: + """The height of the region.""" - class Lattice: - """Represents a lattice of traps in the device.""" + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... - lattice_origin: Device.Vector - """ - The origin of the lattice. - """ - lattice_vector_1: Device.Vector - """ - The first lattice vector. - """ - lattice_vector_2: Device.Vector - """ - The second lattice vector. - """ - sublattice_offsets: Iterable[Device.Vector] - """ - The offsets of the sublattices. - """ - extent: Device.Region - """ - The extent of the lattice. - """ - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - - traps: Iterable[Device.Lattice] - """ - The list of trap positions in the device. - """ - t1: int - """ - The T1 time of the device. - """ - t2: int - """ - The T2 time of the device. - """ - - @classmethod - def try_create_from_device(cls, device: GenericDevice) -> Device | None: + @property + def origin(self) -> Device.Lattice.Vector: + """The origin of the region.""" + + @property + def size(self) -> Device.Lattice.Region.Size: + """The size of the region.""" + + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + + @property + def lattice_origin(self) -> Device.Lattice.Vector: + """The origin of the lattice.""" + + @property + def lattice_vector_1(self) -> Device.Lattice.Vector: + """The first lattice vector.""" + + @property + def lattice_vector_2(self) -> Device.Lattice.Vector: + """The second lattice vector.""" + + @property + def sublattice_offsets(self) -> list[Device.Lattice.Vector]: + """The offsets of the sublattices.""" + + @property + def extent(self) -> Device.Lattice.Region: + """The extent of the lattice.""" + + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + + @property + def traps(self) -> list[Device.Lattice]: + """The list of trap positions in the device.""" + + @property + def t1(self) -> int: + """The T1 time of the device.""" + + @property + def t2(self) -> int: + """The T2 time of the device.""" + + @staticmethod + def try_create_from_device(device: mqt.core.fomac.Device) -> Device | None: """Create NA FoMaC Device from generic FoMaC Device. Args: @@ -110,8 +101,9 @@ class Device(GenericDevice): Returns: The converted NA FoMaC Device or None if the conversion is not possible. """ - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... -def devices() -> Iterable[Device]: + def __eq__(self, arg: object, /) -> bool: ... + def __ne__(self, arg: object, /) -> bool: ... + +def devices() -> list[Device]: """Returns a list of available devices.""" diff --git a/src/ir/operations/IfElseOperation.cpp b/src/ir/operations/IfElseOperation.cpp index 4bd26c9019..539ca0181a 100644 --- a/src/ir/operations/IfElseOperation.cpp +++ b/src/ir/operations/IfElseOperation.cpp @@ -75,9 +75,9 @@ IfElseOperation::IfElseOperation(std::unique_ptr&& thenOp, const ClassicalRegister& controlRegister, const std::uint64_t expectedValue, const ComparisonKind kind) - : thenOp(std::move(thenOp)), elseOp(std::move(elseOp)), - controlRegister(controlRegister), expectedValueRegister(expectedValue), - comparisonKind(kind) { + : thenOp_(std::move(thenOp)), elseOp_(std::move(elseOp)), + controlRegister_(controlRegister), expectedValueRegister_(expectedValue), + comparisonKind_(kind) { name = "if_else"; type = IfElse; canonicalize(); @@ -87,42 +87,42 @@ IfElseOperation::IfElseOperation(std::unique_ptr&& thenOp, std::unique_ptr&& elseOp, const Bit controlBit, const bool expectedValue, const ComparisonKind kind) - : thenOp(std::move(thenOp)), elseOp(std::move(elseOp)), - controlBit(controlBit), expectedValueBit(expectedValue), - comparisonKind(kind) { + : thenOp_(std::move(thenOp)), elseOp_(std::move(elseOp)), + controlBit_(controlBit), expectedValueBit_(expectedValue), + comparisonKind_(kind) { name = "if_else"; type = IfElse; canonicalize(); } IfElseOperation::IfElseOperation(const IfElseOperation& op) - : Operation(op), thenOp(op.thenOp ? op.thenOp->clone() : nullptr), - elseOp(op.elseOp ? op.elseOp->clone() : nullptr), - controlRegister(op.controlRegister), controlBit(op.controlBit), - expectedValueRegister(op.expectedValueRegister), - expectedValueBit(op.expectedValueBit), comparisonKind(op.comparisonKind) { -} + : Operation(op), thenOp_(op.thenOp_ ? op.thenOp_->clone() : nullptr), + elseOp_(op.elseOp_ ? op.elseOp_->clone() : nullptr), + controlRegister_(op.controlRegister_), controlBit_(op.controlBit_), + expectedValueRegister_(op.expectedValueRegister_), + expectedValueBit_(op.expectedValueBit_), + comparisonKind_(op.comparisonKind_) {} IfElseOperation& IfElseOperation::operator=(const IfElseOperation& op) { if (this != &op) { Operation::operator=(op); - thenOp = op.thenOp ? op.thenOp->clone() : nullptr; - elseOp = op.elseOp ? op.elseOp->clone() : nullptr; - controlRegister = op.controlRegister; - controlBit = op.controlBit; - expectedValueRegister = op.expectedValueRegister; - expectedValueBit = op.expectedValueBit; - comparisonKind = op.comparisonKind; + thenOp_ = op.thenOp_ ? op.thenOp_->clone() : nullptr; + elseOp_ = op.elseOp_ ? op.elseOp_->clone() : nullptr; + controlRegister_ = op.controlRegister_; + controlBit_ = op.controlBit_; + expectedValueRegister_ = op.expectedValueRegister_; + expectedValueBit_ = op.expectedValueBit_; + comparisonKind_ = op.comparisonKind_; } return *this; } void IfElseOperation::apply(const Permutation& permutation) { - if (thenOp) { - thenOp->apply(permutation); + if (thenOp_) { + thenOp_->apply(permutation); } - if (elseOp) { - elseOp->apply(permutation); + if (elseOp_) { + elseOp_->apply(permutation); } } @@ -130,33 +130,33 @@ bool IfElseOperation::equals(const Operation& operation, const Permutation& perm1, const Permutation& perm2) const { if (const auto* other = dynamic_cast(&operation)) { - if (controlRegister != other->controlRegister) { + if (controlRegister_ != other->controlRegister_) { return false; } - if (controlBit != other->controlBit) { + if (controlBit_ != other->controlBit_) { return false; } - if (expectedValueRegister != other->expectedValueRegister) { + if (expectedValueRegister_ != other->expectedValueRegister_) { return false; } - if (expectedValueBit != other->expectedValueBit) { + if (expectedValueBit_ != other->expectedValueBit_) { return false; } - if (comparisonKind != other->comparisonKind) { + if (comparisonKind_ != other->comparisonKind_) { return false; } - if (thenOp && other->thenOp) { - if (!thenOp->equals(*other->thenOp, perm1, perm2)) { + if (thenOp_ && other->thenOp_) { + if (!thenOp_->equals(*other->thenOp_, perm1, perm2)) { return false; } - } else if (thenOp || other->thenOp) { + } else if (thenOp_ || other->thenOp_) { return false; } - if (elseOp && other->elseOp) { - if (!elseOp->equals(*other->elseOp, perm1, perm2)) { + if (elseOp_ && other->elseOp_) { + if (!elseOp_->equals(*other->elseOp_, perm1, perm2)) { return false; } - } else if (elseOp || other->elseOp) { + } else if (elseOp_ || other->elseOp_) { return false; } return true; @@ -172,27 +172,27 @@ IfElseOperation::print(std::ostream& os, const Permutation& permutation, // print condition header line os << indent << "\033[1m\033[35m" << "if ("; - if (controlRegister.has_value()) { - assert(!controlBit.has_value()); - os << controlRegister->getName() << ' ' << comparisonKind << ' ' - << expectedValueRegister; - } else if (controlBit.has_value()) { - assert(!controlRegister.has_value()); - os << (!expectedValueBit ? "!" : "") << "c[" << controlBit.value() << "]"; + if (controlRegister_.has_value()) { + assert(!controlBit_.has_value()); + os << controlRegister_->getName() << ' ' << comparisonKind_ << ' ' + << expectedValueRegister_; + } else if (controlBit_.has_value()) { + assert(!controlRegister_.has_value()); + os << (!expectedValueBit_ ? "!" : "") << "c[" << controlBit_.value() << "]"; } os << ") {\033[0m" << '\n'; // cyan brace // then-block - if (thenOp) { + if (thenOp_) { os << indent; - thenOp->print(os, permutation, prefixWidth, nqubits); + thenOp_->print(os, permutation, prefixWidth, nqubits); } os << '\n'; // else-block (only if present) - if (elseOp) { + if (elseOp_) { os << indent << " \033[1m\033[35m} else {\033[0m" << '\n' << indent; - elseOp->print(os, permutation, prefixWidth, nqubits); + elseOp_->print(os, permutation, prefixWidth, nqubits); os << '\n'; } @@ -209,41 +209,41 @@ void IfElseOperation::dumpOpenQASM(std::ostream& of, const bool openQASM3) const { of << std::string(indent * OUTPUT_INDENT_SIZE, ' '); of << "if ("; - if (controlRegister.has_value()) { - assert(!controlBit.has_value()); - of << controlRegister->getName() << ' ' << comparisonKind << ' ' - << expectedValueRegister; - } else if (controlBit.has_value()) { - of << (!expectedValueBit ? "!" : "") << bitMap.at(*controlBit).second; + if (controlRegister_.has_value()) { + assert(!controlBit_.has_value()); + of << controlRegister_->getName() << ' ' << comparisonKind_ << ' ' + << expectedValueRegister_; + } else if (controlBit_.has_value()) { + of << (!expectedValueBit_ ? "!" : "") << bitMap.at(*controlBit_).second; } of << ") "; of << "{\n"; - if (thenOp) { - thenOp->dumpOpenQASM(of, qubitMap, bitMap, indent + 1, openQASM3); + if (thenOp_) { + thenOp_->dumpOpenQASM(of, qubitMap, bitMap, indent + 1, openQASM3); } - if (!elseOp) { + if (!elseOp_) { of << "}\n"; return; } of << "}"; if (openQASM3) { of << " else {\n"; - elseOp->dumpOpenQASM(of, qubitMap, bitMap, indent + 1, openQASM3); + elseOp_->dumpOpenQASM(of, qubitMap, bitMap, indent + 1, openQASM3); } else { of << '\n' << "if ("; - if (controlRegister.has_value()) { - assert(!controlBit.has_value()); - of << controlRegister->getName() << ' ' - << getInvertedComparisonKind(comparisonKind) << ' ' - << expectedValueRegister; + if (controlRegister_.has_value()) { + assert(!controlBit_.has_value()); + of << controlRegister_->getName() << ' ' + << getInvertedComparisonKind(comparisonKind_) << ' ' + << expectedValueRegister_; } - if (controlBit.has_value()) { - assert(!controlRegister.has_value()); - of << (expectedValueBit ? "!" : "") << bitMap.at(*controlBit).second; + if (controlBit_.has_value()) { + assert(!controlRegister_.has_value()); + of << (expectedValueBit_ ? "!" : "") << bitMap.at(*controlBit_).second; } of << ") "; of << "{\n"; - elseOp->dumpOpenQASM(of, qubitMap, bitMap, indent + 1, openQASM3); + elseOp_->dumpOpenQASM(of, qubitMap, bitMap, indent + 1, openQASM3); } of << "}\n"; } @@ -266,27 +266,27 @@ void IfElseOperation::dumpOpenQASM(std::ostream& of, */ void IfElseOperation::canonicalize() { // If thenOp is null, swap thenOp and elseOp, and invert the comparison kind. - if (thenOp == nullptr) { - std::swap(thenOp, elseOp); - comparisonKind = getInvertedComparisonKind(comparisonKind); + if (thenOp_ == nullptr) { + std::swap(thenOp_, elseOp_); + comparisonKind_ = getInvertedComparisonKind(comparisonKind_); } // If control is a single bit, only equality comparisons are supported. - if (controlBit.has_value()) { + if (controlBit_.has_value()) { // Convert Neq to Eq by inverting expectedValueBit. - if (comparisonKind == Neq) { - comparisonKind = Eq; - expectedValueBit = !expectedValueBit; + if (comparisonKind_ == Neq) { + comparisonKind_ = Eq; + expectedValueBit_ = !expectedValueBit_; } // Throw if comparison is not Eq (after possible conversion above). - if (comparisonKind != Eq) { + if (comparisonKind_ != Eq) { throw std::invalid_argument( "Inequality comparisons on a single bit are not supported."); } // If expectedValueBit is false and elseOp exists, swap thenOp and elseOp, // and set expectedValueBit to true. - if (!expectedValueBit && elseOp != nullptr) { - std::swap(thenOp, elseOp); - expectedValueBit = true; + if (!expectedValueBit_ && elseOp_ != nullptr) { + std::swap(thenOp_, elseOp_); + expectedValueBit_ = true; } } } diff --git a/src/qdmi/dd/Device.cpp b/src/qdmi/dd/Device.cpp index 09a165d75e..e771db7477 100644 --- a/src/qdmi/dd/Device.cpp +++ b/src/qdmi/dd/Device.cpp @@ -299,8 +299,8 @@ auto MQT_DDSIM_QDMI_Device_Session_impl_d::querySiteProperty( IS_INVALID_ARGUMENT(prop, QDMI_SITE_PROPERTY)) { return QDMI_ERROR_INVALIDARGUMENT; } - const auto id = - static_cast(reinterpret_cast(site) - OFFSET); + const auto id = reinterpret_cast(site) - OFFSET; + static_assert(sizeof(uintptr_t) == sizeof(size_t)); ADD_SINGLE_VALUE_PROPERTY(QDMI_SITE_PROPERTY_INDEX, size_t, id, prop, size, value, sizeRet) return QDMI_ERROR_NOTSUPPORTED; diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index de27e86d05..b9ddd51d86 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -10,6 +10,7 @@ #include "qir/runtime/Runtime.hpp" +#include "dd/DDDefinitions.hpp" #include "dd/Node.hpp" #include "dd/Package.hpp" #include "dd/StateGeneration.hpp" @@ -90,7 +91,7 @@ auto Runtime::enlargeState(const std::uint64_t maxQubit) -> void { static_cast::difference_type>( numQubitsInQState), qubitPermutation.end(), numQubitsInQState); - numQubitsInQState += d; + numQubitsInQState += static_cast(d); // resize the DD package only if necessary if (dd->qubits() < numQubitsInQState) { @@ -99,7 +100,7 @@ auto Runtime::enlargeState(const std::uint64_t maxQubit) -> void { // if the state is terminal, we need to create a new node if (qState.isTerminal()) { - qState = dd::makeZeroState(d, *dd); + qState = makeZeroState(d, *dd); return; } diff --git a/test/qir/runtime/test_qir_runtime.cpp b/test/qir/runtime/test_qir_runtime.cpp index 34153deca3..ace8a2203f 100644 --- a/test/qir/runtime/test_qir_runtime.cpp +++ b/test/qir/runtime/test_qir_runtime.cpp @@ -508,9 +508,9 @@ INSTANTIATE_TEST_SUITE_P( QIRFilesTest, //< Test suite name // Parameters to test with ::testing::Values(TEST_EXECUTABLES), - [](const testing::TestParamInfo& info) { + [](const testing::TestParamInfo& inf) { // Extract the last part of the file path - auto filename = info.param.stem().string(); + auto filename = inf.param.stem().string(); // replace all '-' with '_' std::ranges::replace(filename, '-', '_'); return filename; diff --git a/uv.lock b/uv.lock index a680b75e53..f1dcb5182c 100644 --- a/uv.lock +++ b/uv.lock @@ -1497,15 +1497,15 @@ qiskit = [ [package.dev-dependencies] build = [ - { name = "pybind11" }, + { name = "nanobind" }, { name = "scikit-build-core" }, { name = "setuptools-scm" }, ] dev = [ { name = "lit" }, + { name = "nanobind" }, { name = "nox" }, { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "pybind11" }, { name = "pytest" }, { name = "pytest-console-scripts" }, { name = "pytest-cov" }, @@ -1550,16 +1550,16 @@ provides-extras = ["qiskit"] [package.metadata.requires-dev] build = [ - { name = "pybind11", specifier = ">=3.0.1" }, + { name = "nanobind", specifier = ">=2.10.2" }, { name = "scikit-build-core", specifier = ">=0.11.6" }, { name = "setuptools-scm", specifier = ">=9.2.2" }, ] dev = [ { name = "lit", specifier = ">=18.1.8" }, + { name = "nanobind", specifier = ">=2.10.2" }, { name = "nox", specifier = ">=2025.11.12" }, { name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1" }, { name = "numpy", marker = "python_full_version >= '3.14'", specifier = ">=2.3.2" }, - { name = "pybind11", specifier = ">=3.0.1" }, { name = "pytest", specifier = ">=9.0.1" }, { name = "pytest-console-scripts", specifier = ">=1.4.1" }, { name = "pytest-cov", specifier = ">=7.0.0" }, @@ -1640,6 +1640,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, ] +[[package]] +name = "nanobind" +version = "2.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/7b/818fe4f6d1fdd516a14386ba86f2cbbac1b7304930da0f029724e9001658/nanobind-2.10.2.tar.gz", hash = "sha256:08509910ce6d1fadeed69cb0880d4d4fcb77739c6af9bd8fb4419391a3ca4c6b", size = 993651, upload-time = "2025-12-10T10:55:32.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/06/cb08965f985a5e1b9cb55ed96337c1f6daaa6b9cbdaeabe6bb3f7a1a11df/nanobind-2.10.2-py3-none-any.whl", hash = "sha256:6976c1b04b90481d2612b346485a3063818c6faa5077fe9d8bbc9b5fbe29c380", size = 246514, upload-time = "2025-12-10T10:55:30.741Z" }, +] + [[package]] name = "nbclient" version = "0.10.3" @@ -2147,15 +2156,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] -[[package]] -name = "pybind11" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/7b/a6d8dcb83c457e24a9df1e4d8fd5fb8034d4bbc62f3c324681e8a9ba57c2/pybind11-3.0.1.tar.gz", hash = "sha256:9c0f40056a016da59bab516efb523089139fcc6f2ba7e4930854c61efb932051", size = 546914, upload-time = "2025-08-22T20:09:27.265Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/8a/37362fc2b949d5f733a8b0f2ff51ba423914cabefe69f1d1b6aab710f5fe/pybind11-3.0.1-py3-none-any.whl", hash = "sha256:aa8f0aa6e0a94d3b64adfc38f560f33f15e589be2175e103c0a33c6bce55ee89", size = 293611, upload-time = "2025-08-22T20:09:25.235Z" }, -] - [[package]] name = "pybtex" version = "0.25.1"