diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06810b840..a67774baf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -209,9 +209,10 @@ jobs: if: fromJSON(needs.change-detection.outputs.run-python-tests) uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-linter.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11 with: - setup-z3: true - enable-ty: true + check-stubs: true enable-mypy: false + enable-ty: true + setup-z3: true build-sdist: name: 🚀 CD diff --git a/.license-tools-config.json b/.license-tools-config.json index 741379c13..9c7609caf 100644 --- a/.license-tools-config.json +++ b/.license-tools-config.json @@ -37,6 +37,7 @@ ".*\\.profile", "uv\\.lock", "py\\.typed", - ".*build.*" + ".*build.*", + "qmap_patterns.txt" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e2f7dd4..422cf83ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ ### Changed - 🔧 Replace `mypy` with `ty` ([#912]) ([**@denialhaag**]) -- ♻️ Migrate Python bindings from `pybind11` to `nanobind` ([#911]) ([**@denialhaag**]) +- ♻️ Migrate Python bindings from `pybind11` to `nanobind` ([#911], [#916]) ([**@denialhaag**]) - 📦️ Provide Stable ABI wheels for Python 3.12+ ([#911]) ([**@denialhaag**]) ## [3.5.0] - 2025-12-16 @@ -188,6 +188,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ +[#916]: https://github.com/munich-quantum-toolkit/qmap/pull/916 [#912]: https://github.com/munich-quantum-toolkit/qmap/pull/912 [#911]: https://github.com/munich-quantum-toolkit/qmap/pull/911 [#902]: https://github.com/munich-quantum-toolkit/qmap/pull/902 diff --git a/UPGRADING.md b/UPGRADING.md index 1bdcacc06..1f840d912 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -4,6 +4,15 @@ This document describes breaking changes and how to upgrade. For a complete list ## [Unreleased] +### Renamings + +To comply with established guidelines for function and attribute names, this release includes the following renamings: + +- `mqt.qmap.plugins.qiskit.sc.compile` has been renamed to `compile_`. +- `mqt.qmap.sc.map` has been renamed to `map_`. +- `mqt.qmap.sc.Configuration.include_WCNF` has been renamed to `include_wcnf`. +- `mqt.qmap.sc.MappingResult.input` has been renamed to `input_`. + ### Stable ABI wheels We are now providing Stable ABI wheels instead of separate version-specific wheels for Python 3.12+. diff --git a/bindings/clifford_synthesis/clifford_synthesis.cpp b/bindings/clifford_synthesis/clifford_synthesis.cpp index c52c650c0..2ea4d9f4e 100644 --- a/bindings/clifford_synthesis/clifford_synthesis.cpp +++ b/bindings/clifford_synthesis/clifford_synthesis.cpp @@ -50,9 +50,9 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { .value("verbose", plog::Severity::verbose, "Show all information."); // Configuration for the synthesis - nb::class_( - m, "SynthesisConfiguration", - "Configuration options for the MQT QMAP Clifford synthesis tool.") + nb::class_(m, "SynthesisConfiguration", + "Class representing the configuration for the " + "Clifford synthesis techniques.") .def(nb::init<>()) .def_rw("initial_timestep_limit", &cs::Configuration::initialTimestepLimit, @@ -68,20 +68,20 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { "is known.") .def_rw( "use_maxsat", &cs::Configuration::useMaxSAT, - "Use MaxSAT to solve the synthesis problem or to really on the " - "binary search scheme for finding the optimum. Defaults to `false`.") + "Use MaxSAT to solve the synthesis problem or to rely on the " + "binary search scheme for finding the optimum. Defaults to `False`.") .def_rw("linear_search", &cs::Configuration::linearSearch, - "Use liner search instead of binary search " - "scheme for finding the optimum. Defaults to `false`.") + "Use linear search instead of binary search " + "scheme for finding the optimum. Defaults to `False`.") .def_rw("target_metric", &cs::Configuration::target, "Target metric for the Clifford synthesis. Defaults to `gates`.") .def_rw("use_symmetry_breaking", &cs::Configuration::useSymmetryBreaking, "Use symmetry breaking clauses to speed up the synthesis " - "process. Defaults to `true`.") + "process. Defaults to `True`.") .def_rw("dump_intermediate_results", &cs::Configuration::dumpIntermediateResults, "Dump intermediate results of the synthesis process. " - "Defaults to `false`.") + "Defaults to `False`.") .def_rw("intermediate_results_path", &cs::Configuration::intermediateResultsPath, "Path to the directory where intermediate results should " @@ -98,7 +98,7 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { &cs::Configuration::minimizeGatesAfterDepthOptimization, "Depth optimization might produce a circuit with more gates than " "necessary. This option enables an additional run of the synthesizer " - "to minimize the overall number of gates. Defaults to `false`.") + "to minimize the overall number of gates. Defaults to `False`.") .def_rw( "try_higher_gate_limit_for_two_qubit_gate_optimization", &cs::Configuration::tryHigherGateLimitForTwoQubitGateOptimization, @@ -106,7 +106,7 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { "to find an optimal solution for a certain timestep limit, but there " "might be a better solution for some higher timestep limit. This " "option enables an additional run of the synthesizer with a higher " - "gate limit. Defaults to `false`.") + "gate limit. Defaults to `False`.") .def_rw("gate_limit_factor", &cs::Configuration::gateLimitFactor, "Factor by which the gate limit is increased when " "trying to find a better solution for the two-qubit " @@ -116,11 +116,11 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { "Two-qubit gate optimization might produce a circuit " "with more gates than necessary. This option enables " "an additional run of the synthesizer to minimize the " - "overall number of gates. Defaults to `false`.") + "overall number of gates. Defaults to `False`.") .def_rw("heuristic", &cs::Configuration::heuristic, "Use heuristic to synthesize the circuit. " "This method synthesizes shallow intermediate circuits " - "and combines them. Defaults to `false`.") + "and combines them. Defaults to `False`.") .def_rw("split_size", &cs::Configuration::splitSize, "Size of subcircuits used in heuristic. " "Defaults to `5`.") @@ -130,9 +130,10 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { .def( "json", [](const cs::Configuration& config) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); - return loads(config.json().dump()); + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); + const auto dict = loads(config.json().dump()); + return nb::cast>(dict); }, "Returns a JSON-style dictionary of all the information present in " "the :class:`.Configuration`") @@ -143,8 +144,9 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { "present in the :class:`.Configuration`"); // Results of the synthesis - nb::class_(m, "SynthesisResults", - "Results of the MQT QMAP Clifford synthesis tool.") + nb::class_( + m, "SynthesisResults", + "Class representing the results of the Clifford synthesis techniques.") .def(nb::init<>()) .def_prop_ro("gates", &cs::Results::getGates, "Returns the number of gates in the circuit.") @@ -166,12 +168,12 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { "Returns a string representation of the " "synthesized circuit's tableau.") .def("sat", &cs::Results::sat, - "Returns `true` if the synthesis was successful.") + "Returns `True` if the synthesis was successful.") .def("unsat", &cs::Results::unsat, - "Returns `true` if the synthesis was unsuccessful."); + "Returns `True` if the synthesis was unsuccessful."); auto tableau = nb::class_( - m, "Tableau", "A class for representing stabilizer tableaus."); + m, "Tableau", "Class representing a Clifford tableau."); tableau.def(nb::init(), "n"_a, "include_destabilizers"_a = false, "Creates a tableau for an n-qubit Clifford."); @@ -182,11 +184,12 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { tableau.def( nb::init(), "stabilizers"_a, "destabilizers"_a, - "Constructs a tableau from two lists of Pauli strings, the Stabilizers" - "and Destabilizers."); + "Constructs a tableau from two lists of Pauli strings, the stabilizers " + "and destabilizers."); auto synthesizer = nb::class_( - m, "CliffordSynthesizer", "A class for synthesizing Clifford circuits."); + m, "CliffordSynthesizer", + "The main class for the Clifford synthesis techniques."); synthesizer.def(nb::init(), "initial_tableau"_a, "target_tableau"_a, @@ -210,7 +213,11 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { synthesizer.def_prop_ro("results", &cs::CliffordSynthesizer::getResults, nb::rv_policy::reference_internal, "Returns the results of the synthesis."); - synthesizer.def_prop_ro("result_circuit", [](cs::CliffordSynthesizer& self) { - return qasm3::Importer::imports(self.getResults().getResultCircuit()); - }); + synthesizer.def_prop_ro( + "result_circuit", + [](cs::CliffordSynthesizer& self) { + return qasm3::Importer::imports(self.getResults().getResultCircuit()); + }, + "Returns the synthesized circuit as a " + ":class:`~mqt.core.ir.QuantumComputation` object."); } diff --git a/bindings/na/CMakeLists.txt b/bindings/na/CMakeLists.txt index abb4880e2..71edf5450 100644 --- a/bindings/na/CMakeLists.txt +++ b/bindings/na/CMakeLists.txt @@ -6,8 +6,20 @@ # # Licensed under the MIT License -add_subdirectory(nasp) -add_subdirectory(zoned) +file(GLOB_RECURSE NA_SOURCES **.cpp) + +add_mqt_python_binding_nanobind( + QMAP + ${MQT_QMAP_TARGET_NAME}-na-bindings + ${NA_SOURCES} + MODULE_NAME + na + INSTALL_DIR + . + LINK_LIBS + MQT::NASP + MQT::QMapNAZoned + MQT::CoreQASM) # Install the Python stub files in editable mode for better IDE support if(SKBUILD_STATE STREQUAL "editable") diff --git a/bindings/na/na.cpp b/bindings/na/na.cpp new file mode 100644 index 000000000..6d052b9d6 --- /dev/null +++ b/bindings/na/na.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include + +namespace nb = nanobind; + +// forward declarations +void registerStatePreparation(nb::module_& m); +void registerZoned(nb::module_& m); + +NB_MODULE(MQT_QMAP_MODULE_NAME, m) { + auto statePreparation = m.def_submodule("state_preparation"); + registerStatePreparation(statePreparation); + + auto zoned = m.def_submodule("zoned"); + registerZoned(zoned); +} diff --git a/bindings/na/nasp/CMakeLists.txt b/bindings/na/nasp/CMakeLists.txt deleted file mode 100644 index db65ff63d..000000000 --- a/bindings/na/nasp/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -add_mqt_python_binding_nanobind( - QMAP - ${MQT_QMAP_TARGET_NAME}-na-nasp-bindings - nasp.cpp - MODULE_NAME - state_preparation - INSTALL_DIR - ./na - LINK_LIBS - MQT::NASP - MQT::CoreQASM) diff --git a/bindings/na/nasp/nasp.cpp b/bindings/na/nasp/nasp.cpp deleted file mode 100644 index 80f4d7cbc..000000000 --- a/bindings/na/nasp/nasp.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "ir/QuantumComputation.hpp" -#include "ir/operations/OpType.hpp" -#include "na/nasp/CodeGenerator.hpp" -#include "na/nasp/Solver.hpp" - -#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 - -namespace nb = nanobind; -using namespace nb::literals; - -NB_MODULE(MQT_QMAP_MODULE_NAME, m) { - nb::module_::import_("mqt.core.ir"); - - // Neutral Atom State Preparation - nb::class_(m, "NAStatePreparationSolver") - .def(nb::init(), - "max_x"_a, "max_y"_a, "max_c"_a, "max_r"_a, "max_h_offset"_a, - "max_v_offset"_a, "max_h_dist"_a, "max_v_dist"_a, - "min_entangling_y"_a, "max_entangling_y"_a) - .def("solve", &na::NASolver::solve, "ops"_a, "num_qubits"_a, - "num_stages"_a, "num_transfers"_a = nb::none(), - "mind_ops_order"_a = false, "shield_idle_qubits"_a = true); - - nb::class_(m, "NAStatePreparationSolver.Result") - .def(nb::init<>()) - .def("json", [](const na::NASolver::Result& result) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); - return loads(result.json().dump()); - }); - - m.def( - "generate_code", - [](const qc::QuantumComputation& qc, const na::NASolver::Result& result, - const uint16_t minAtomDist, const uint16_t noInteractionRadius, - const uint16_t zoneDist) { - return na::CodeGenerator::generate(qc, result, minAtomDist, - noInteractionRadius, zoneDist) - .toString(); - }, - "qc"_a, "result"_a, "min_atom_dist"_a = 1, "no_interaction_radius"_a = 10, - "zone_dist"_a = 24); - - m.def( - "get_ops_for_solver", - [](const qc::QuantumComputation& qc, const std::string& operationType, - const uint64_t numControls, const bool quiet) { - auto opTypeLowerStr = operationType; - std::ranges::transform(opTypeLowerStr, opTypeLowerStr.begin(), - [](unsigned char c) { return std::tolower(c); }); - return na::NASolver::getOpsForSolver( - qc, qc::opTypeFromString(operationType), numControls, quiet); - }, - "qc"_a, "operation_type"_a = "Z", "num_operands"_a = 1, "quiet"_a = true); -} diff --git a/bindings/na/register_state_preparation.cpp b/bindings/na/register_state_preparation.cpp new file mode 100644 index 000000000..f9a1e1f2d --- /dev/null +++ b/bindings/na/register_state_preparation.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ir/QuantumComputation.hpp" +#include "ir/operations/OpType.hpp" +#include "na/nasp/CodeGenerator.hpp" +#include "na/nasp/Solver.hpp" + +#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 + +namespace nb = nanobind; +using namespace nb::literals; + +// NOLINTNEXTLINE(misc-use-internal-linkage) +void registerStatePreparation(nb::module_& m) { + nb::module_::import_("mqt.core.ir"); + + // Neutral Atom State Preparation + auto solver = + nb::class_(m, "NAStatePreparationSolver", + R"pb(Neutral atom state preparation solver. + +The neutral atom state preparation solver generates an optimal sequence of neutral atom operations for a given state preparation circuit.)pb") + .def( + nb::init(), + "max_x"_a, "max_y"_a, "max_c"_a, "max_r"_a, "max_h_offset"_a, + "max_v_offset"_a, "max_h_dist"_a, "max_v_dist"_a, + "min_entangling_y"_a, "max_entangling_y"_a, + R"pb(Create a solver instance for the neutral atom state preparation problem. + +The solver is based on a 2D grid abstraction of the neutral atom quantum computer. +The 2D plane is divided into interaction sites. +Each interaction site is denoted by abstract x- and y-coordinates. +The parameter `max_x` specifies the maximum x-coordinate, and `max_y` specifies the maximum y-coordinate. +In the center of an interaction site, sits an SLM trap. +Around that trap there are several possible discrete AOD positions arranged as a grid. +The specific position of an atom within an interaction site is determined by the x- and y-offset from the SLM trap, i.e., those can be positive and negative. +The maximum absolute value of the x- and y-offset is specified by `max_h_offset` and `max_v_offset`, respectively. +Then, the parameter `max_c` specifies the maximum number of AOD columns, and `max_r` specifies the maximum number of AOD rows. +Finally, in order to interact during a Rydberg stage, atoms must be located within a certain distance. +The maximum horizontal and vertical distance between two atoms is specified by `max_h_dist` and `max_v_dist`, respectively. +The parameter `min_entangling_y` specifies the minimum y-coordinate for entangling operations, and `max_entangling_y` specifies the maximum y-coordinate for entangling operations. +Hence, y-coordinates outside of this range are located in the storage zone. + +Note: + The solver can only handle a single storage zone below the entangling zone, i.e., in this case `min_entangling_y` must be zero and `max_entangling_y` must be less than `max_y`. + +Args: + max_x: The maximum discrete x-coordinate of the interaction sites + max_y: The maximum discrete y-coordinate of the interaction sites + max_c: The maximum number of AOD columns + max_r: The maximum number of AOD rows + max_h_offset: The maximum horizontal offset of the atoms + max_v_offset: The maximum vertical offset of the atoms + max_h_dist: The maximum horizontal distance between two atoms + max_v_dist: The maximum vertical distance between two atoms + min_entangling_y: The minimum y-coordinate for entangling operations + max_entangling_y: The maximum y-coordinate for entangling operations + +Raises: + ValueError: If one of the parameters is invalid, e.g., is a negative value)pb") + + .def("solve", &na::NASolver::solve, "ops"_a, "num_qubits"_a, + "num_stages"_a, "num_transfers"_a = nb::none(), + "mind_ops_order"_a = false, "shield_idle_qubits"_a = true, + R"pb(Solve the neutral atom state preparation problem. + +The solver generates an optimal sequence of neutral atom operations for a given state preparation circuit. +The circuit is given as a list of operations, where each operation is a pair of qubits. +The sequence is divided into stages. +Each stage is either a Rydberg stage or a transfer stage. +In a Rydberg stage, adjacent qubits in the entangling zone undergo an entangling gate. +In a transfer stage, atoms can be stored from AOD into SLM traps and loaded from SLM traps into AOD. +At the end of each stage, the atoms are shuttled to their next position. +The number of stages is specified by `num_stages`. +The number of transfers is fixed by `num_transfers` if given. +If this parameter is not specified, then the solver will determine the optimal number of transfers. +The parameter `mind_ops_order` specifies whether the order of the operations in the circuit should be preserved. +The parameter `shield_idle_qubits` specifies whether idle qubits should be shielded from the entangling operations. + +Note: + To retrieve the list of qubit pairs from a quantum circuit, use the function :func:`get_ops_for_solver`. + + The returned solver's result can either be directly exported to the JSON format by calling the method :func:`json` on the result object or the result object can be passed to the function :func:`generate_code` to generate code consisting of neutral atom operations. + +Args: + ops: The list of operations in the circuit + num_qubits: The number of qubits in the circuit + num_stages: The number of stages in the sequence + num_transfers: The number of transfers in the sequence. Can be `None` + mind_ops_order: Whether the order of the operations should be preserved + shield_idle_qubits: Whether idle qubits should be shielded + +Returns: + The result of the solver + +Raises: + ValueError: if one of the numeral parameters is invalid, e.g., is a negative value)pb"); + + nb::class_( + solver, "Result", "The result of a :class:`~.NAStatePreparationSolver`.") + .def(nb::init<>(), "Create a result object.") + .def( + "json", + [](const na::NASolver::Result& result) { + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); + const auto dict = loads(result.json().dump()); + return nb::cast>(dict); + }, + R"pb(Returns the result as JSON-style dictionary. + +Returns: + The result as a JSON-style dictionary)pb"); + + m.def( + "generate_code", + [](const qc::QuantumComputation& qc, const na::NASolver::Result& result, + const uint16_t minAtomDist, const uint16_t noInteractionRadius, + const uint16_t zoneDist) { + return na::CodeGenerator::generate(qc, result, minAtomDist, + noInteractionRadius, zoneDist) + .toString(); + }, + "qc"_a, "result"_a, "min_atom_dist"_a = 1, "no_interaction_radius"_a = 10, + "zone_dist"_a = 24, + R"pb(Generate code for the given circuit using the solver's result. + +Some parameters of the abstraction from the 2D grid used for the solver must be provided again. + +Args: + qc: The quantum circuit + result: The result of the solver + min_atom_dist: The minimum distance between atoms + no_interaction_radius: The radius around an atom where no other atom can be placed during an entangling operation that should not interact with the atom + zone_dist: The distance between zones, i.e., the minimal distance between two atoms in different zones + +Returns: + The generated code as a string + +Raises: + ValueError: If one of the numeral parameters is invalid, e.g., is a negative value)pb"); + + m.def( + "get_ops_for_solver", + [](const qc::QuantumComputation& qc, const std::string& operationType, + const uint64_t numControls, const bool quiet) { + auto operationTypeLower = operationType; + std::ranges::transform(operationTypeLower, operationTypeLower.begin(), + [](unsigned char c) { return std::tolower(c); }); + return na::NASolver::getOpsForSolver( + qc, qc::opTypeFromString(operationTypeLower), numControls, quiet); + }, + "qc"_a, "operation_type"_a = "Z", "num_controls"_a = 1, "quiet"_a = true, + R"pb(Extract entangling operations as list of qubit pairs from the circuit. + +Note: + This function can only extract qubit pairs of two-qubit operations. + I.e., the operands of the operation plus the controls must be equal to two. + +Args: + qc: The quantum circuit + operation_type: The type of operation to extract, e.g., "z" for CZ gates + num_controls: The number of controls the operation acts on, e.g., 1 for CZ gates + quiet: Whether to suppress warnings when the circuit contains operations other than the specified operation type + +Returns: + List of qubit pairs + +Raises: + ValueError: If the circuit contains operations other than the specified operation type and quiet is False + ValueError: If the operation has more than two operands including controls)pb"); +} diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/register_zoned.cpp similarity index 58% rename from bindings/na/zoned/zoned.cpp rename to bindings/na/register_zoned.cpp index 88e994340..272575cd2 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/register_zoned.cpp @@ -31,47 +31,93 @@ namespace nb = nanobind; using namespace nb::literals; -// NOLINTNEXTLINE(performance-unnecessary-value-param) -NB_MODULE(MQT_QMAP_MODULE_NAME, m) { +// NOLINTNEXTLINE(misc-use-internal-linkage) +void registerZoned(nb::module_& m) { nb::module_::import_("mqt.core.ir"); nb::class_ architecture( - m, "ZonedNeutralAtomArchitecture"); + m, "ZonedNeutralAtomArchitecture", + "Class representing a zoned neutral atom architecture."); + architecture.def_static("from_json_file", - &na::zoned::Architecture::fromJSONFile, "filename"_a); + &na::zoned::Architecture::fromJSONFile, "filename"_a, + R"pb(Create an architecture from a JSON file. + +Args: + filename: The path to the JSON file + +Returns: + The architecture + +Raises: + ValueError: if the file does not exist or is not a valid JSON file)pb"); + architecture.def_static("from_json_string", - &na::zoned::Architecture::fromJSONString, "json"_a); + &na::zoned::Architecture::fromJSONString, "json"_a, + R"pb(Create an architecture from a JSON string. + +Args: + json: The JSON string + +Returns: + The architecture + +Raises: + ValueError: If the string is not a valid JSON string)pb"); architecture.def( "to_namachine_file", [](na::zoned::Architecture& self, const std::string& filename) -> void { self.exportNAVizMachine(filename); }, - "filename"_a); - architecture.def("to_namachine_string", - [](na::zoned::Architecture& self) -> std::string { - return self.exportNAVizMachine(); - }); + "filename"_a, R"pb(Write the architecture to a .namachine file. + +Args: + filename: The path to the .namachine file)pb"); + + architecture.def( + "to_namachine_string", + [](na::zoned::Architecture& self) -> std::string { + return self.exportNAVizMachine(); + }, + R"pb(Get the architecture as a .namachine string. + +Returns: + The architecture as a .namachine string)pb"); //===--------------------------------------------------------------------===// // Placement Method Enum //===--------------------------------------------------------------------===// - nb::enum_(m, "PlacementMethod") - .value("astar", na::zoned::HeuristicPlacer::Config::Method::ASTAR) - .value("ids", na::zoned::HeuristicPlacer::Config::Method::IDS); + nb::enum_( + m, "PlacementMethod", + "Enumeration of the available placement methods for the heuristic " + "placer.") + .value("astar", na::zoned::HeuristicPlacer::Config::Method::ASTAR, + "A-star algorithm.") + .value("ids", na::zoned::HeuristicPlacer::Config::Method::IDS, + "Iterative diving search."); //===--------------------------------------------------------------------===// // Routing Method Enum //===--------------------------------------------------------------------===// - nb::enum_(m, "RoutingMethod") - .value("strict", na::zoned::IndependentSetRouter::Config::Method::STRICT) + nb::enum_( + m, "RoutingMethod", + "Enumeration of the available routing methods for the independent set " + "router.") + .value("strict", na::zoned::IndependentSetRouter::Config::Method::STRICT, + "Strict routing, i.e., the relative order of atoms must be " + "maintained throughout a movement.") .value("relaxed", - na::zoned::IndependentSetRouter::Config::Method::RELAXED); + na::zoned::IndependentSetRouter::Config::Method::RELAXED, + "Relaxed routing, i.e., the relative order of atoms may change " + "throughout a movement by applying offsets during pick-up and " + "drop-off."); //===--------------------------------------------------------------------===// // Routing-agnostic Compiler //===--------------------------------------------------------------------===// nb::class_ routingAgnosticCompiler( - m, "RoutingAgnosticCompiler"); + m, "RoutingAgnosticCompiler", + "Routing-agnostic zoned neutral atom compiler."); { const na::zoned::RoutingAgnosticCompiler::Config defaultConfig; routingAgnosticCompiler.def( @@ -109,8 +155,21 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { "prefer_split"_a = defaultConfig.layoutSynthesizerConfig.routerConfig.preferSplit, "warn_unsupported_gates"_a = - defaultConfig.codeGeneratorConfig.warnUnsupportedGates); + defaultConfig.codeGeneratorConfig.warnUnsupportedGates, + R"pb(Create a routing-agnostic compiler for the given architecture and configurations. + +Args: + arch: The zoned neutral atom architecture + log_level: The log level for the compiler, possible values are "debug"/"D", "info"/"I", "warning"/"W", "error"/"E", and "critical"/"C" + max_filling_factor: The maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel + use_window: Whether to use a window for the placer + window_size: The size of the window for the placer + dynamic_placement: Whether to use dynamic placement for the placer + routing_method: The routing method that should be used for the independent set router + prefer_split: The threshold factor for group merging decisions during routing. + warn_unsupported_gates: Whether to warn about unsupported gates in the code generator)pb"); } + routingAgnosticCompiler.def_static( "from_json_string", [](const na::zoned::Architecture& arch, @@ -120,27 +179,53 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { // NOLINTNEXTLINE(misc-include-cleaner) return {arch, nlohmann::json::parse(json)}; }, - "arch"_a, "json"_a); + "arch"_a, "json"_a, + R"pb(Create a compiler for the given architecture and with configurations from a JSON string. + +Args: + arch: The zoned neutral atom architecture + json: The JSON string + +Returns: + The initialized compiler + +Raises: + ValueError: If the string is not a valid JSON string)pb"); + routingAgnosticCompiler.def( "compile", [](na::zoned::RoutingAgnosticCompiler& self, const qc::QuantumComputation& qc) -> std::string { return self.compile(qc).toString(); }, - "qc"_a); + "qc"_a, + R"pb(Compile a quantum circuit for the zoned neutral atom architecture. + +Args: + qc: The quantum circuit + +Returns: + The compilation result as a string in the .naviz format.)pb"); + routingAgnosticCompiler.def( - "stats", [](const na::zoned::RoutingAgnosticCompiler& self) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); + "stats", + [](const na::zoned::RoutingAgnosticCompiler& self) { + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); const nlohmann::json stats = self.getStatistics(); - return loads(stats.dump()); - }); + const auto dict = loads(stats.dump()); + return nb::cast>(dict); + }, + R"pb(Get the statistics of the last compilation as a JSON-style dictionary. + +Returns: + The statistics as a JSON-style dictionary)pb"); //===--------------------------------------------------------------------===// // Routing-aware Compiler //===--------------------------------------------------------------------===// nb::class_ routingAwareCompiler( - m, "RoutingAwareCompiler"); + m, "RoutingAwareCompiler", "Routing-aware zoned neutral atom compiler."); { const na::zoned::RoutingAwareCompiler::Config defaultConfig; routingAwareCompiler.def( @@ -211,8 +296,33 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { "prefer_split"_a = defaultConfig.layoutSynthesizerConfig.routerConfig.preferSplit, "warn_unsupported_gates"_a = - defaultConfig.codeGeneratorConfig.warnUnsupportedGates); + defaultConfig.codeGeneratorConfig.warnUnsupportedGates, + R"pb(Create a routing-aware compiler for the given architecture and configurations. + +Args: + arch: The zoned neutral atom architecture + log_level: The log level for the compiler, possible values are "debug"/"D", "info"/"I", "warning"/"W", "error"/"E", and "critical"/"C" + max_filling_factor: The maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel + use_window: Whether to use a window for the placer + window_min_width: The minimum width of the window for the placer + window_ratio: The ratio between the height and the width of the window + window_share: The share of free sites in the window in relation to the number of atoms to be moved in this step + placement_method: The placement method that should be used for the heuristic placer + deepening_factor: Controls the impact of the term in the heuristic of the A* search that resembles the standard deviation of the differences between the current and target sites of the atoms to be moved in every orientation + deepening_value: Is added to the sum of standard deviations before it is multiplied with the number of unplaced nodes and :attr:`deepening_factor` + lookahead_factor: Controls the lookahead's influence that considers the distance of atoms to their interaction partner in the next layer + reuse_level: The reuse level that corresponds to the estimated extra fidelity loss due to the extra trap transfers when the atom is not reused and instead moved to the storage zone and back to the entanglement zone + max_nodes: The maximum number of nodes that are considered in the A* search. + If this number is exceeded, the search is aborted and an error is raised. + In the current implementation, one node roughly consumes 120 Byte. + Hence, allowing 50,000,000 nodes results in memory consumption of about 6 GB plus the size of the rest of the data structures. + trials: The number of restarts during IDS. + queue_capacity: The maximum capacity of the priority queue used during IDS. + routing_method: The routing method that should be used for the independent set router + prefer_split: The threshold factor for group merging decisions during routing. + warn_unsupported_gates: Whether to warn about unsupported gates in the code generator)pb"); } + routingAwareCompiler.def_static( "from_json_string", [](const na::zoned::Architecture& arch, @@ -222,19 +332,45 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { // NOLINTNEXTLINE(misc-include-cleaner) return {arch, nlohmann::json::parse(json)}; }, - "arch"_a, "json"_a); + "arch"_a, "json"_a, + R"pb(Create a compiler for the given architecture and configurations from a JSON string. + +Args: + arch: The zoned neutral atom architecture + json: The JSON string + +Returns: + The initialized compiler + +Raises: + ValueError: If the string is not a valid JSON string)pb"); + routingAwareCompiler.def( "compile", [](na::zoned::RoutingAwareCompiler& self, const qc::QuantumComputation& qc) -> std::string { return self.compile(qc).toString(); }, - "qc"_a); + "qc"_a, + R"pb(Compile a quantum circuit for the zoned neutral atom architecture. + +Args: + qc: The quantum circuit + +Returns: + The compilation result as a string in the .naviz format.)pb"); + routingAwareCompiler.def( - "stats", [](const na::zoned::RoutingAwareCompiler& self) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); + "stats", + [](const na::zoned::RoutingAwareCompiler& self) { + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); const nlohmann::json stats = self.getStatistics(); - return loads(stats.dump()); - }); + const auto dict = loads(stats.dump()); + return nb::cast>(dict); + }, + R"pb(Get the statistics of the last compilation. + +Returns: + The statistics as a dictionary)pb"); } diff --git a/bindings/na/zoned/CMakeLists.txt b/bindings/na/zoned/CMakeLists.txt deleted file mode 100644 index 7c6d583bb..000000000 --- a/bindings/na/zoned/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -add_mqt_python_binding_nanobind( - QMAP - ${MQT_QMAP_TARGET_NAME}-na-zoned-bindings - zoned.cpp - MODULE_NAME - zoned - INSTALL_DIR - ./na - LINK_LIBS - MQT::QMapNAZoned - MQT::CoreQASM) diff --git a/bindings/qmap_patterns.txt b/bindings/qmap_patterns.txt new file mode 100644 index 000000000..55bcfafb2 --- /dev/null +++ b/bindings/qmap_patterns.txt @@ -0,0 +1,3 @@ +_hashable_values_: + +_unhashable_values_map_: diff --git a/bindings/sc/sc.cpp b/bindings/sc/sc.cpp index 54407f595..611cba121 100644 --- a/bindings/sc/sc.cpp +++ b/bindings/sc/sc.cpp @@ -174,7 +174,7 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { // All configuration options for QMAP nb::class_( m, "Configuration", - "Configuration options for the MQT QMAP quantum circuit mapping tool") + "Class representing the configuration for the mapping.") .def(nb::init<>()) .def_rw("method", &Configuration::method) .def_rw("heuristic", &Configuration::heuristic) @@ -200,7 +200,7 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { .def_rw("encoding", &Configuration::encoding) .def_rw("commander_grouping", &Configuration::commanderGrouping) .def_rw("use_subsets", &Configuration::useSubsets) - .def_rw("include_WCNF", &Configuration::includeWCNF) + .def_rw("include_wcnf", &Configuration::includeWCNF) .def_rw("enable_limits", &Configuration::enableSwapLimits) .def_rw("swap_reduction", &Configuration::swapReduction) .def_rw("swap_limit", &Configuration::swapLimit) @@ -215,18 +215,18 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { &Configuration::addBarriersBetweenLayers) .def("json", [](const Configuration& config) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); - return loads(config.json().dump()); + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); + const auto dict = loads(config.json().dump()); + return nb::cast>(dict); }) .def("__repr__", &Configuration::toString); // Results of the mapping process - nb::class_( - m, "MappingResults", - "Results of the MQT QMAP quantum circuit mapping tool") + nb::class_(m, "MappingResults", + "Class representing the results of a mapping.") .def(nb::init<>()) - .def_rw("input", &MappingResults::input) + .def_rw("input_", &MappingResults::input) .def_rw("output", &MappingResults::output) .def_rw("configuration", &MappingResults::config) .def_rw("time", &MappingResults::time) @@ -238,15 +238,16 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { .def_rw("wcnf", &MappingResults::wcnf) .def("json", [](const MappingResults& results) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); - return loads(results.json().dump()); + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); + const auto dict = loads(results.json().dump()); + return nb::cast>(dict); }) .def("__repr__", &MappingResults::toString); // Main class for storing circuit information - nb::class_(m, "CircuitInfo", - "Circuit information") + nb::class_( + m, "CircuitInfo", "Class containing circuit information.") .def(nb::init<>()) .def_rw("name", &MappingResults::CircuitInfo::name) .def_rw("qubits", &MappingResults::CircuitInfo::qubits) @@ -277,9 +278,10 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { .def_rw("effective_branching_factor", &MappingResults::HeuristicBenchmarkInfo::effectiveBranchingFactor) .def("json", [](const MappingResults::HeuristicBenchmarkInfo& info) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); - return loads(info.json().dump()); + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); + const auto dict = loads(info.json().dump()); + return nb::cast>(dict); }); // Heuristic benchmark information for individual layers @@ -314,15 +316,16 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { .def_rw("early_termination", &MappingResults::LayerHeuristicBenchmarkInfo::earlyTermination) .def("json", [](const MappingResults::LayerHeuristicBenchmarkInfo& info) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); - return loads(info.json().dump()); + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); + const auto dict = loads(info.json().dump()); + return nb::cast>(dict); }); auto arch = nb::class_( - m, "Architecture", "Class representing device/backend information"); + m, "Architecture", "Class representing device/backend information."); auto properties = nb::class_( - arch, "Properties", "Class representing properties of an architecture"); + arch, "Properties", "Class representing properties of an architecture."); // Properties of an architecture (e.g. number of qubits, connectivity, error // rates, ...) @@ -405,9 +408,10 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { .def( "json", [](const Architecture::Properties& props) { - const nb::module_ json = nb::module_::import_("json"); - const nb::object loads = json.attr("loads"); - return loads(props.json().dump()); + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); + const auto dict = loads(props.json().dump()); + return nb::cast>(dict); }, "Returns a JSON-style dictionary of all the information present in " "the :class:`.Properties`") @@ -448,5 +452,14 @@ NB_MODULE(MQT_QMAP_MODULE_NAME, m) { "properties"_a); // Main mapping function - m.def("map", &map, "map a quantum circuit", "circ"_a, "arch"_a, "config"_a); + m.def("map_", &map, "circ"_a, "arch"_a, "config"_a, + R"pb(Map a quantum circuit to an architecture. + +Args: + circ: The quantum circuit to map. + arch: The architecture to map to. + config: The mapping configuration. + +Returns: + A tuple containing the mapped circuit and the mapping results.)pb"); } diff --git a/docs/mapping.md b/docs/mapping.md index 67e610077..608764c45 100644 --- a/docs/mapping.md +++ b/docs/mapping.md @@ -130,10 +130,10 @@ On directional architectures, it can be significantly cheaper to surround a CNOT Using the exact mapper is as simple as: ```{code-cell} ipython3 -from mqt.qmap.plugins.qiskit.sc import compile +from mqt.qmap.plugins.qiskit.sc import compile_ from mqt.qmap.sc import Method -qc_mapped, res = compile(qc, arch, method=Method.exact, post_mapping_optimizations=False) +qc_mapped, res = compile_(qc, arch, method=Method.exact, post_mapping_optimizations=False) qc_mapped.draw(output="mpl") ``` @@ -149,7 +149,7 @@ The resulting solution only requires _two_ SWAP gates for mapping the circuit. The exact mapping method implemented in QMAP is optimal with respect to the number of additional SWAP gates needed for mapping a given circuit. It is not guaranteed to be optimal with respect to the number of additional gates needed for mapping a given circuit, e.g., any sequence of a SWAP gate and a CNOT gate acting on the same qubits can be simplified to just two CNOT gates. -Such an optimization pass is conducted by default in the `compile` method after the circuit has been mapped. +Such an optimization pass is conducted by default in the `compile_` function after the circuit has been mapped. However, this cost reduction is not accounted for in the SAT formulation at the moment. ``` @@ -161,7 +161,7 @@ This allows to reliably determine suitable mappings for circuits with up to hund Using the heuristic mapper works completely analogous to the exact mapper. ```{code-cell} ipython3 -qc_mapped, res = compile(qc, arch, method=Method.heuristic, post_mapping_optimizations=False) +qc_mapped, res = compile_(qc, arch, method=Method.heuristic, post_mapping_optimizations=False) qc_mapped.draw(output="mpl") ``` diff --git a/noxfile.py b/noxfile.py index 0c8745f57..5bc951abd 100755 --- a/noxfile.py +++ b/noxfile.py @@ -21,6 +21,7 @@ import shutil import sys import tempfile +from pathlib import Path from typing import TYPE_CHECKING import nox @@ -190,5 +191,60 @@ 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" / "qmap" + pattern_file = Path(__file__).parent / "bindings" / "qmap_patterns.txt" + + session.run( + "python", + "-m", + "nanobind.stubgen", + "--recursive", + "--include-private", + "--output-dir", + str(package_root), + "--pattern-file", + str(pattern_file), + "--module", + "mqt.qmap.na", + "--module", + "mqt.qmap.clifford_synthesis", + "--module", + "mqt.qmap.hybrid_mapper", + "--module", + "mqt.qmap.sc", + ) + + pyi_files = list(package_root.glob("**/*.pyi")) + + if not pyi_files: + session.warn("No .pyi files found") + return + + if shutil.which("prek") is None: + session.install("prek") + + # Allow both 0 (no issues) and 1 as success codes for fixing up stubs + success_codes = [0, 1] + session.run("prek", "run", "license-tools", "--files", *pyi_files, external=True, success_codes=success_codes) + session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True, success_codes=success_codes) + session.run("prek", "run", "ruff-format", "--files", *pyi_files, external=True, success_codes=success_codes) + + # Run ruff-check again to ensure everything is clean + session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True) + + if __name__ == "__main__": nox.main() diff --git a/pyproject.toml b/pyproject.toml index f05d231d0..df1cb978e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,8 +96,7 @@ build-dir = "build/{wheel_tag}/{build_type}" build.targets = [ "mqt-qmap-clifford_synthesis-bindings", "mqt-qmap-hybrid_mapper-bindings", - "mqt-qmap-na-zoned-bindings", - "mqt-qmap-na-nasp-bindings", + "mqt-qmap-na-bindings", "mqt-qmap-sc-bindings", ] @@ -268,7 +267,7 @@ known-first-party = ["mqt.qmap"] "docs/**" = ["T20"] "eval/**" = ["T20"] "noxfile.py" = ["T20", "TID251"] -"*.pyi" = ["D418", "PYI021"] # "*.pyi" = ["D418", "DOC202", "PYI011", "PYI021"] +"*.pyi" = ["D418", "E501", "PYI021"] "*.ipynb" = [ "D", # pydocstyle "E402", # Allow imports to appear anywhere in Jupyter notebooks diff --git a/python/mqt/qmap/clifford_synthesis.pyi b/python/mqt/qmap/clifford_synthesis.pyi index aa0da5c36..fdbfd1907 100644 --- a/python/mqt/qmap/clifford_synthesis.pyi +++ b/python/mqt/qmap/clifford_synthesis.pyi @@ -6,92 +6,236 @@ # # Licensed under the MIT License -"""Python bindings for Clifford synthesis module.""" - -from enum import Enum +import enum +from collections.abc import Mapping from typing import Any, overload -from mqt.core.ir import QuantumComputation +import mqt.core.ir + +class TargetMetric(enum.Enum): + gates = 0 + """Optimize gate count.""" + + two_qubit_gates = 1 + """Optimize two-qubit gate count.""" + + depth = 2 + """Optimize circuit depth.""" + +class Verbosity(enum.Enum): + none = 0 + """No output.""" + + fatal = 1 + """Only show fatal errors.""" + + error = 2 + """Show errors.""" + + warning = 3 + """Show warnings.""" -class TargetMetric(Enum): - depth = ... - gates = ... - two_qubit_gates = ... + info = 4 + """Show general information.""" -class Verbosity(Enum): - none = ... - fatal = ... - error = ... - warning = ... - info = ... - debug = ... - verbose = ... + debug = 5 + """Show additional debug information.""" + + verbose = 6 + """Show all information.""" class SynthesisConfiguration: """Class representing the configuration for the Clifford synthesis techniques.""" - dump_intermediate_results: bool - gate_limit_factor: float - initial_timestep_limit: int - intermediate_results_path: str - minimize_gates_after_depth_optimization: bool - minimize_gates_after_two_qubit_gate_optimization: bool - solver_parameters: dict[str, bool | int | float | str] - target_metric: TargetMetric - try_higher_gate_limit_for_two_qubit_gate_optimization: bool - use_maxsat: bool - use_symmetry_breaking: bool - verbosity: Verbosity - heuristic: bool - split_size: int - linear_search: bool - def __init__(self) -> None: ... - def json(self) -> dict[str, Any]: ... + @property + def initial_timestep_limit(self) -> int: + """Initial timestep limit for the Clifford synthesis. Defaults to `0`, which implies that the initial timestep limit is determined automatically.""" + + @initial_timestep_limit.setter + def initial_timestep_limit(self, arg: int, /) -> None: ... + @property + def minimal_timesteps(self) -> int: + """Minimal timestep considered for the Clifford synthesis. This option limits the lower bound of the interval in which the binary search method looks for solutions. Set this if you know a lower bound for the circuit depth. Defaults to `0`, which implies that no lower bound for depth is known.""" + + @minimal_timesteps.setter + def minimal_timesteps(self, arg: int, /) -> None: ... + @property + def use_maxsat(self) -> bool: + """Use MaxSAT to solve the synthesis problem or to rely on the binary search scheme for finding the optimum. Defaults to `False`.""" + + @use_maxsat.setter + def use_maxsat(self, arg: bool, /) -> None: ... + @property + def linear_search(self) -> bool: + """Use linear search instead of binary search scheme for finding the optimum. Defaults to `False`.""" + + @linear_search.setter + def linear_search(self, arg: bool, /) -> None: ... + @property + def target_metric(self) -> TargetMetric: + """Target metric for the Clifford synthesis. Defaults to `gates`.""" + + @target_metric.setter + def target_metric(self, arg: TargetMetric, /) -> None: ... + @property + def use_symmetry_breaking(self) -> bool: + """Use symmetry breaking clauses to speed up the synthesis process. Defaults to `True`.""" + + @use_symmetry_breaking.setter + def use_symmetry_breaking(self, arg: bool, /) -> None: ... + @property + def dump_intermediate_results(self) -> bool: + """Dump intermediate results of the synthesis process. Defaults to `False`.""" + + @dump_intermediate_results.setter + def dump_intermediate_results(self, arg: bool, /) -> None: ... + @property + def intermediate_results_path(self) -> str: + """Path to the directory where intermediate results should be dumped. Defaults to `./`. The path needs to include a path separator at the end.""" + + @intermediate_results_path.setter + def intermediate_results_path(self, arg: str, /) -> None: ... + @property + def verbosity(self) -> Verbosity: + """Verbosity level for the synthesis process. Defaults to 'warning'.""" + + @verbosity.setter + def verbosity(self, arg: Verbosity, /) -> None: ... + @property + def solver_parameters(self) -> dict[str, bool | int | float | str]: + """Parameters to be passed to Z3 as dict[str, bool | int | float | str].""" + + @solver_parameters.setter + def solver_parameters(self, arg: Mapping[str, bool | int | float | str], /) -> None: ... + @property + def minimize_gates_after_depth_optimization(self) -> bool: + """Depth optimization might produce a circuit with more gates than necessary. This option enables an additional run of the synthesizer to minimize the overall number of gates. Defaults to `False`.""" + + @minimize_gates_after_depth_optimization.setter + def minimize_gates_after_depth_optimization(self, arg: bool, /) -> None: ... + @property + def try_higher_gate_limit_for_two_qubit_gate_optimization(self) -> bool: + """When optimizing two-qubit gates, the synthesizer might fail to find an optimal solution for a certain timestep limit, but there might be a better solution for some higher timestep limit. This option enables an additional run of the synthesizer with a higher gate limit. Defaults to `False`.""" + + @try_higher_gate_limit_for_two_qubit_gate_optimization.setter + def try_higher_gate_limit_for_two_qubit_gate_optimization(self, arg: bool, /) -> None: ... + @property + def gate_limit_factor(self) -> float: + """Factor by which the gate limit is increased when trying to find a better solution for the two-qubit gate optimization. Defaults to `1.1`.""" + + @gate_limit_factor.setter + def gate_limit_factor(self, arg: float, /) -> None: ... + @property + def minimize_gates_after_two_qubit_gate_optimization(self) -> bool: + """Two-qubit gate optimization might produce a circuit with more gates than necessary. This option enables an additional run of the synthesizer to minimize the overall number of gates. Defaults to `False`.""" + + @minimize_gates_after_two_qubit_gate_optimization.setter + def minimize_gates_after_two_qubit_gate_optimization(self, arg: bool, /) -> None: ... + @property + def heuristic(self) -> bool: + """Use heuristic to synthesize the circuit. This method synthesizes shallow intermediate circuits and combines them. Defaults to `False`.""" + + @heuristic.setter + def heuristic(self, arg: bool, /) -> None: ... + @property + def split_size(self) -> int: + """Size of subcircuits used in heuristic. Defaults to `5`.""" + + @split_size.setter + def split_size(self, arg: int, /) -> None: ... + @property + def n_threads_heuristic(self) -> int: + """Maximum number of threads used for the heuristic optimizer. Defaults to the number of available threads on the system.""" + + @n_threads_heuristic.setter + def n_threads_heuristic(self, arg: int, /) -> None: ... + def json(self) -> dict[str, Any]: + """Returns a JSON-style dictionary of all the information present in the :class:`.Configuration`.""" class SynthesisResults: """Class representing the results of the Clifford synthesis techniques.""" + def __init__(self) -> None: ... - def sat(self) -> bool: ... - def unsat(self) -> bool: ... @property - def circuit(self) -> str: ... + def gates(self) -> int: + """Returns the number of gates in the circuit.""" + @property - def depth(self) -> int: ... + def single_qubit_gates(self) -> int: + """Returns the number of single-qubit gates in the synthesized circuit.""" + @property - def gates(self) -> int: ... + def two_qubit_gates(self) -> int: + """Returns the number of two-qubit gates in the synthesized circuit.""" + @property - def runtime(self) -> float: ... + def depth(self) -> int: + """Returns the depth of the synthesized circuit.""" + @property - def single_qubit_gates(self) -> int: ... + def runtime(self) -> float: + """Returns the runtime of the synthesis in seconds.""" + @property - def solver_calls(self) -> int: ... + def solver_calls(self) -> int: + """Returns the number of calls to the SAT solver.""" + @property - def tableau(self) -> str: ... + def circuit(self) -> str: + """Returns the synthesized circuit as a qasm string.""" + @property - def two_qubit_gates(self) -> int: ... + def tableau(self) -> str: + """Returns a string representation of the synthesized circuit's tableau.""" + + def sat(self) -> bool: + """Returns `True` if the synthesis was successful.""" + + def unsat(self) -> bool: + """Returns `True` if the synthesis was unsuccessful.""" class Tableau: """Class representing a Clifford tableau.""" + @overload - def __init__(self, n: int, include_stabilizers: bool = False) -> None: ... + def __init__(self, n: int, include_destabilizers: bool = False) -> None: + """Creates a tableau for an n-qubit Clifford.""" + @overload - def __init__(self, description: str) -> None: ... + def __init__(self, tableau: str) -> None: + """Constructs a tableau from a string description. This can either be a semicolon separated binary matrix or a list of Pauli strings.""" + @overload - def __init__(self, stabilizers: str, destabilizers: str) -> None: ... + def __init__(self, stabilizers: str, destabilizers: str) -> None: + """Constructs a tableau from two lists of Pauli strings, the stabilizers and destabilizers.""" class CliffordSynthesizer: """The main class for the Clifford synthesis techniques.""" + @overload - def __init__(self, initial_tableau: Tableau, target_tableau: Tableau) -> None: ... + def __init__(self, initial_tableau: Tableau, target_tableau: Tableau) -> None: + """Constructs a synthesizer for two tableaus representing the initial and target state.""" + @overload - def __init__(self, target_tableau: Tableau) -> None: ... + def __init__(self, target_tableau: Tableau) -> None: + """Constructs a synthesizer for a tableau representing the target state.""" + @overload - def __init__(self, qc: QuantumComputation, use_destabilizers: bool) -> None: ... + def __init__(self, qc: mqt.core.ir.QuantumComputation, use_destabilizers: bool) -> None: + """Constructs a synthesizer for a quantum computation representing the target state.""" + @overload - def __init__(self, initial_tableau: Tableau, qc: QuantumComputation) -> None: ... - def synthesize(self, config: SynthesisConfiguration = ...) -> None: ... + def __init__(self, initial_tableau: Tableau, qc: mqt.core.ir.QuantumComputation) -> None: + """Constructs a synthesizer for a quantum computation representing the target state that starts in an initial state represented by a tableau.""" + + def synthesize(self, config: SynthesisConfiguration = ...) -> None: + """Runs the synthesis with the given configuration.""" + @property - def results(self) -> SynthesisResults: ... + def results(self) -> SynthesisResults: + """Returns the results of the synthesis.""" + @property - def result_circuit(self) -> QuantumComputation: ... + def result_circuit(self) -> mqt.core.ir.QuantumComputation: + """Returns the synthesized circuit as a :class:`~mqt.core.ir.QuantumComputation` object.""" diff --git a/python/mqt/qmap/hybrid_mapper.pyi b/python/mqt/qmap/hybrid_mapper.pyi index 8da167ea5..d4dcc6c18 100644 --- a/python/mqt/qmap/hybrid_mapper.pyi +++ b/python/mqt/qmap/hybrid_mapper.pyi @@ -6,117 +6,314 @@ # # Licensed under the MIT License -"""Python bindings for hybrid mapper module.""" +import enum +from collections.abc import Sequence -import typing -from enum import Enum +import mqt.core.ir -from mqt.core.ir import QuantumComputation +class InitialCoordinateMapping(enum.Enum): + """Initial mapping between hardware qubits hardware coordinates.""" -class InitialCoordinateMapping(Enum): - random = ... - trivial = ... + trivial = 0 + """Trivial identity mapping.""" -class InitialCircuitMapping(Enum): - identity = ... - graph = ... + random = 1 + """Random mapping.""" + +class InitialCircuitMapping(enum.Enum): + """Initial mapping between circuit qubits and hardware qubits.""" + + identity = 0 + """Identity mapping.""" + + graph = 1 + """Graph matching mapping.""" class MapperParameters: - decay: float - dynamic_mapping_weight: float - gate_weight: float - initial_coord_mapping: InitialCoordinateMapping - limit_shuttling_layer: int - lookahead_depth: int - lookahead_weight_moves: float - lookahead_weight_swaps: float - max_bridge_distance: int - num_flying_ancillas: int - seed: int - shuttling_time_weight: float - shuttling_weight: float - use_pass_by: bool - verbose: bool - def __init__(self) -> None: ... + """Parameters controlling the mapper behavior.""" + + def __init__(self) -> None: + """Create a MapperParameters instance with default values.""" + + @property + def lookahead_depth(self) -> int: + """Depth of lookahead for mapping decisions.""" + + @lookahead_depth.setter + def lookahead_depth(self, arg: int, /) -> None: ... + @property + def lookahead_weight_swaps(self) -> float: + """Weight assigned to swap operations during lookahead.""" + + @lookahead_weight_swaps.setter + def lookahead_weight_swaps(self, arg: float, /) -> None: ... + @property + def lookahead_weight_moves(self) -> float: + """Weight assigned to move operations during lookahead.""" + + @lookahead_weight_moves.setter + def lookahead_weight_moves(self, arg: float, /) -> None: ... + @property + def decay(self) -> float: + """Decay factor for gate blocking.""" + + @decay.setter + def decay(self, arg: float, /) -> None: ... + @property + def shuttling_time_weight(self) -> float: + """Weight for shuttling time in cost evaluation.""" + + @shuttling_time_weight.setter + def shuttling_time_weight(self, arg: float, /) -> None: ... + @property + def dynamic_mapping_weight(self) -> float: + """Weight for dynamic remapping (SWAPs or MOVEs) in cost evaluation.""" + + @dynamic_mapping_weight.setter + def dynamic_mapping_weight(self, arg: float, /) -> None: ... + @property + def gate_weight(self) -> float: + """Weight for gate execution in cost evaluation.""" + + @gate_weight.setter + def gate_weight(self, arg: float, /) -> None: ... + @property + def shuttling_weight(self) -> float: + """Weight for shuttling operations in cost evaluation.""" + + @shuttling_weight.setter + def shuttling_weight(self, arg: float, /) -> None: ... + @property + def seed(self) -> int: + """Random seed for stochastic decisions (initial mapping, etc.).""" + + @seed.setter + def seed(self, arg: int, /) -> None: ... + @property + def num_flying_ancillas(self) -> int: + """Number of ancilla qubits to be used (0 or 1 for now).""" + + @num_flying_ancillas.setter + def num_flying_ancillas(self, arg: int, /) -> None: ... + @property + def limit_shuttling_layer(self) -> int: + """Maximum allowed shuttling layer (default: 10).""" + + @limit_shuttling_layer.setter + def limit_shuttling_layer(self, arg: int, /) -> None: ... + @property + def max_bridge_distance(self) -> int: + """Maximum distance for bridge operations.""" + + @max_bridge_distance.setter + def max_bridge_distance(self, arg: int, /) -> None: ... + @property + def use_pass_by(self) -> bool: + """Enable or disable pass-by operations.""" + + @use_pass_by.setter + def use_pass_by(self, arg: bool, /) -> None: ... + @property + def verbose(self) -> bool: + """Enable verbose logging for debugging.""" + + @verbose.setter + def verbose(self, arg: bool, /) -> None: ... + @property + def initial_coord_mapping(self) -> InitialCoordinateMapping: + """Strategy for initial coordinate mapping.""" + + @initial_coord_mapping.setter + def initial_coord_mapping(self, arg: InitialCoordinateMapping, /) -> None: ... class MapperStats: - num_bridges: int - num_f_ancillas: int - num_moves: int - num_pass_by: int - num_swaps: int def __init__(self) -> None: ... + @property + def num_swaps(self) -> int: + """Number of swap operations performed.""" + + @num_swaps.setter + def num_swaps(self, arg: int, /) -> None: ... + @property + def num_bridges(self) -> int: + """Number of bridge operations performed.""" + + @num_bridges.setter + def num_bridges(self, arg: int, /) -> None: ... + @property + def num_f_ancillas(self) -> int: + """Number of fresh ancilla qubits used.""" + + @num_f_ancillas.setter + def num_f_ancillas(self, arg: int, /) -> None: ... + @property + def num_moves(self) -> int: + """Number of move operations performed.""" + + @num_moves.setter + def num_moves(self, arg: int, /) -> None: ... + @property + def num_pass_by(self) -> int: + """Number of pass-by operations performed.""" + + @num_pass_by.setter + def num_pass_by(self, arg: int, /) -> None: ... class NeutralAtomHybridArchitecture: - name: str def __init__(self, filename: str) -> None: ... - def compute_swap_distance(self, idx1: typing.SupportsInt, idx2: typing.SupportsInt) -> int: ... - def get_gate_average_fidelity(self, s: str) -> float: ... - def get_gate_time(self, s: str) -> float: ... - def get_nearby_coordinates(self, idx: typing.SupportsInt) -> set[int]: ... def load_json(self, json_filename: str) -> None: ... @property - def blocking_factor(self) -> float: ... + def name(self) -> str: + """Name of the architecture.""" + + @name.setter + def name(self, arg: str, /) -> None: ... @property - def decoherence_time(self) -> float: ... + def num_rows(self) -> int: + """Number of rows in a rectangular grid SLM arrangement.""" + @property - def inter_qubit_distance(self) -> float: ... + def num_columns(self) -> int: + """Number of columns in a rectangular grid SLM arrangement.""" + @property - def interaction_radius(self) -> float: ... + def num_positions(self) -> int: + """Total number of positions in a rectangular grid SLM arrangement.""" + @property - def naod_coordinates(self) -> int: ... + def num_aods(self) -> int: + """Number of independent 2D acousto-optic deflectors.""" + @property - def naod_intermediate_levels(self) -> int: ... + def num_qubits(self) -> int: + """Number of atoms in the neutral atom quantum computer that can be used as qubits.""" + @property - def num_aods(self) -> int: ... + def inter_qubit_distance(self) -> float: + """Distance between SLM traps in micrometers.""" + @property - def num_columns(self) -> int: ... + def interaction_radius(self) -> float: + """Interaction radius in inter-qubit distances.""" + @property - def num_positions(self) -> int: ... + def blocking_factor(self) -> float: + """Blocking factor for parallel Rydberg gates.""" + @property - def num_qubits(self) -> int: ... + def naod_intermediate_levels(self) -> int: + """Number of possible AOD positions between two SLM traps.""" + @property - def num_rows(self) -> int: ... + def decoherence_time(self) -> float: + """Decoherence time in microseconds.""" + + def compute_swap_distance(self, idx1: int, idx2: int) -> int: + """Number of SWAP gates required between two positions.""" + + def get_gate_time(self, s: str) -> float: + """Execution time of certain gate in microseconds.""" + + def get_gate_average_fidelity(self, s: str) -> float: + """Average gate fidelity from [0,1].""" + + def get_nearby_coordinates(self, idx: int) -> set[int]: + """Positions that are within the interaction radius of the passed position.""" class HybridNAMapper: - def __init__(self, arch: NeutralAtomHybridArchitecture, params: MapperParameters) -> None: ... - def get_animation_viz(self) -> str: ... - def get_init_hw_pos(self) -> dict[int, int]: ... - def get_mapped_qc_qasm(self) -> str: ... - def get_mapped_qc_aod_qasm(self) -> str: ... - def get_stats(self) -> dict[str, float]: ... - def map(self, circ: QuantumComputation, initial_mapping: InitialCircuitMapping = ...) -> None: ... - def map_qasm_file(self, filename: str, initial_mapping: InitialCircuitMapping = ...) -> None: ... - def save_animation_files(self, filename: str) -> None: ... - def save_mapped_qc_aod_qasm(self, filename: str) -> None: ... + """Neutral Atom Hybrid Mapper that can use both SWAP gates and AOD movements to map a quantum circuit to a neutral atom quantum computer.""" + + def __init__(self, arch: NeutralAtomHybridArchitecture, params: MapperParameters) -> None: + """Create Hybrid NA Mapper with mapper parameters.""" + + def set_parameters(self, params: MapperParameters) -> None: + """Set the parameters for the Hybrid NA Mapper.""" + + def get_init_hw_pos(self) -> dict[int, int]: + """Get the initial hardware positions, required to create an animation.""" + + def map(self, qc: mqt.core.ir.QuantumComputation, initial_mapping: InitialCircuitMapping = ...) -> None: + """Map a quantum circuit object to the neutral atom quantum computer.""" + + def map_qasm_file(self, filename: str, initial_mapping: InitialCircuitMapping = ...) -> None: + """Map a quantum circuit to the neutral atom quantum computer.""" + + def get_stats(self) -> dict[str, float]: + """Returns the statistics of the mapping.""" + + def get_mapped_qc_qasm(self) -> str: + """Returns the mapped circuit as an extended qasm2 string.""" + + def get_mapped_qc_aod_qasm(self) -> str: + """Returns the mapped circuit with AOD operations as an extended qasm2 string.""" + + def save_mapped_qc_aod_qasm(self, filename: str) -> None: + """Saves the mapped circuit with AOD operations as an extended qasm2 string.""" + def schedule( - self, - verbose: bool = ..., - create_animation_csv: bool = ..., - shuttling_speed_factor: typing.SupportsFloat = ..., - ) -> dict[str, float]: ... - def set_parameters(self, params: MapperParameters) -> None: ... + self, verbose: bool = False, create_animation_csv: bool = False, shuttling_speed_factor: float = 1.0 + ) -> dict[str, float]: + """Schedule the mapped circuit.""" + + def save_animation_files(self, filename: str) -> None: + """Saves the animation files (.naviz and .namachine) for the scheduling.""" + + def get_animation_viz(self) -> str: + """Returns the .naviz event-log content for the last scheduling.""" -# noinspection DuplicatedCode class HybridSynthesisMapper: + """Neutral Atom Mapper that can evaluate different synthesis steps to choose the best one.""" + def __init__( - self, arch: NeutralAtomHybridArchitecture, params: MapperParameters = ..., buffer_size: typing.SupportsInt = ... - ) -> None: ... - def append_with_mapping(self, qc: QuantumComputation, complete_remap: bool = ...) -> None: ... - def complete_remap(self, include_buffer: bool = ...) -> None: ... - def convert_to_aod(self) -> None: ... + self, arch: NeutralAtomHybridArchitecture, params: MapperParameters = ..., buffer_size: int = 0 + ) -> None: + """Create Hybrid Synthesis Mapper with mapper parameters.""" + + def set_parameters(self, params: MapperParameters) -> None: + """Set the parameters for the Hybrid Synthesis Mapper.""" + + def init_mapping(self, n_qubits: int) -> None: + """Initializes the synthesized and mapped circuits and mapping structures for the given number of qubits.""" + + def get_mapped_qc_qasm(self) -> str: + """Returns the mapped circuit as an extended qasm2 string.""" + + def save_mapped_qc_qasm(self, filename: str) -> None: + """Saves the mapped circuit as an extended qasm2 to a file.""" + + def convert_to_aod(self) -> mqt.core.ir.QuantumComputation: + """Converts the mapped circuit to native AOD movements.""" + + def get_mapped_qc_aod_qasm(self) -> str: + """Returns the mapped circuit with native AOD movements as an extended qasm2 string.""" + + def save_mapped_qc_aod_qasm(self, filename: str) -> None: + """Saves the mapped circuit with native AOD movements as an extended qasm2 to a file.""" + + def get_synthesized_qc_qasm(self) -> str: + """Returns the synthesized circuit with all gates but not mapped to the hardware as a qasm2 string.""" + + def save_synthesized_qc_qasm(self, filename: str) -> None: + """Saves the synthesized circuit with all gates but not mapped to the hardware as qasm2 to a file.""" + + def append_with_mapping(self, qc: mqt.core.ir.QuantumComputation, complete_remap: bool = False) -> None: + """Appends the given QuantumComputation to the synthesized QuantumComputation and maps the gates to the hardware.""" + + def get_circuit_adjacency_matrix(self) -> list[list[int]]: + """Returns the current circuit-qubit adjacency matrix used for mapping.""" + def evaluate_synthesis_steps( - self, synthesis_steps: list[QuantumComputation], complete_remap: bool = ..., also_map: bool = ... - ) -> list[float]: ... - def get_circuit_adjacency_matrix(self) -> list[list[int]]: ... - def get_mapped_qc_aod_qasm(self) -> str: ... - def get_mapped_qc_qasm(self) -> str: ... - def get_synthesized_qc_qasm(self) -> str: ... - def init_mapping(self, n_qubits: typing.SupportsInt) -> None: ... - def save_mapped_qc_aod_qasm(self, filename: str) -> None: ... - def save_mapped_qc_qasm(self, filename: str) -> None: ... - def save_synthesized_qc_qasm(self, filename: str) -> None: ... + self, + synthesis_steps: Sequence[mqt.core.ir.QuantumComputation], + complete_remap: bool = False, + also_map: bool = False, + ) -> list[float]: + """Evaluates the synthesis steps proposed by the ZX extraction. Returns a list of fidelities of the mapped synthesis steps.""" + + def complete_remap(self, include_buffer: bool = True) -> None: + """Remaps the QuantumComputation to the hardware.""" + def schedule( - self, verbose: bool = ..., create_animation_csv: bool = ..., shuttling_speed_factor: typing.SupportsFloat = ... - ) -> dict[str, float]: ... - def set_parameters(self, params: MapperParameters) -> None: ... + self, verbose: bool = False, create_animation_csv: bool = False, shuttling_speed_factor: float = 1.0 + ) -> dict[str, float]: + """Schedule the mapped circuit.""" diff --git a/python/mqt/qmap/na/__init__.py b/python/mqt/qmap/na/__init__.pyi similarity index 72% rename from python/mqt/qmap/na/__init__.py rename to python/mqt/qmap/na/__init__.pyi index 52b0cb236..5dc473cb8 100644 --- a/python/mqt/qmap/na/__init__.py +++ b/python/mqt/qmap/na/__init__.pyi @@ -6,4 +6,5 @@ # # Licensed under the MIT License -"""The MQT QMAP Neutral Atom Package.""" +from . import state_preparation as state_preparation +from . import zoned as zoned diff --git a/python/mqt/qmap/na/state_preparation.pyi b/python/mqt/qmap/na/state_preparation.pyi index 20e515469..40647abb4 100644 --- a/python/mqt/qmap/na/state_preparation.pyi +++ b/python/mqt/qmap/na/state_preparation.pyi @@ -6,17 +6,15 @@ # # Licensed under the MIT License -"""Python bindings module for MQT QMAP's Neutral Atom State Preparation.""" - +from collections.abc import Sequence from typing import Any -from mqt.core.ir import QuantumComputation +import mqt.core.ir class NAStatePreparationSolver: - """The MQT QMAP's Neutral Atom State Preparation Solver. + """Neutral atom state preparation solver. - The neutral atom state preparation solver generates an optimal sequence of - neutral atom operations for a given state preparation circuit. + The neutral atom state preparation solver generates an optimal sequence of neutral atom operations for a given state preparation circuit. """ def __init__( @@ -34,164 +32,140 @@ class NAStatePreparationSolver: ) -> None: """Create a solver instance for the neutral atom state preparation problem. - The solver is based on a 2D grid abstraction of the neutral atom quantum - computer. The 2D plane is divided into interaction sites. Each interaction site - is denoted by abstract x- and y-coordinates. The parameter `max_x` specifies the - maximum x-coordinate, and `max_y` specifies the maximum y-coordinate. In the - center of an interaction site, sits an SLM trap. Around that trap there are - several possible discrete AOD positions arranged as a grid. The specific - position of an atom within an interaction site is determined by the x- and - y-offset from the SLM trap, i.e., those can be positive and negative. The - maximum absolute value of the x- and y-offset is specified by `max_h_offset` and - `max_v_offset`, respectively. Then, the parameter `max_c` specifies the maximum - number of AOD columns, and `max_r` specifies the maximum number of AOD rows. - Finally, in order to interact during a Rydberg stage, atoms must be located - within a certain distance. The maximum horizontal and vertical distance between - two atoms is specified by `max_h_dist` and `max_v_dist`, respectively. The - parameter `min_entangling_y` specifies the minimum y-coordinate for entangling - operations, and `max_entangling_y` specifies the maximum y-coordinate for - entangling operations. Hence, y-coordinates outside of this range are located in - the storage zone. + The solver is based on a 2D grid abstraction of the neutral atom quantum computer. + The 2D plane is divided into interaction sites. + Each interaction site is denoted by abstract x- and y-coordinates. + The parameter `max_x` specifies the maximum x-coordinate, and `max_y` specifies the maximum y-coordinate. + In the center of an interaction site, sits an SLM trap. + Around that trap there are several possible discrete AOD positions arranged as a grid. + The specific position of an atom within an interaction site is determined by the x- and y-offset from the SLM trap, i.e., those can be positive and negative. + The maximum absolute value of the x- and y-offset is specified by `max_h_offset` and `max_v_offset`, respectively. + Then, the parameter `max_c` specifies the maximum number of AOD columns, and `max_r` specifies the maximum number of AOD rows. + Finally, in order to interact during a Rydberg stage, atoms must be located within a certain distance. + The maximum horizontal and vertical distance between two atoms is specified by `max_h_dist` and `max_v_dist`, respectively. + The parameter `min_entangling_y` specifies the minimum y-coordinate for entangling operations, and `max_entangling_y` specifies the maximum y-coordinate for entangling operations. + Hence, y-coordinates outside of this range are located in the storage zone. Note: - The solver can only handle a single storage zone below the entangling zone, - i.e., in this case `min_entangling_y` must be zero and `max_entangling_y` - must be less than `max_y`. + The solver can only handle a single storage zone below the entangling zone, i.e., in this case `min_entangling_y` must be zero and `max_entangling_y` must be less than `max_y`. Args: - max_x: is the maximum discrete x-coordinate of the interaction sites - max_y: is the maximum discrete y-coordinate of the interaction sites - max_c: is the maximum number of AOD columns - max_r: is the maximum number of AOD rows - max_h_offset: is the maximum horizontal offset of the atoms - max_v_offset: is the maximum vertical offset of the atoms - max_h_dist: is the maximum horizontal distance between two atoms - max_v_dist: is the maximum vertical distance between two atoms - min_entangling_y: is the minimum y-coordinate for entangling operations - max_entangling_y: is the maximum y-coordinate for entangling operations + max_x: The maximum discrete x-coordinate of the interaction sites + max_y: The maximum discrete y-coordinate of the interaction sites + max_c: The maximum number of AOD columns + max_r: The maximum number of AOD rows + max_h_offset: The maximum horizontal offset of the atoms + max_v_offset: The maximum vertical offset of the atoms + max_h_dist: The maximum horizontal distance between two atoms + max_v_dist: The maximum vertical distance between two atoms + min_entangling_y: The minimum y-coordinate for entangling operations + max_entangling_y: The maximum y-coordinate for entangling operations Raises: - ValueError: if one of the parameters is invalid, e.g., is a negative - value + ValueError: If one of the parameters is invalid, e.g., is a negative value """ - class Result: - """Neutral Atom State Preparation Solver Result.""" - def __init__(self) -> None: - """Create a result object.""" - def json(self) -> dict[str, Any]: - """Returns the result as a JSON string. - - Returns: - the result as a JSON string - """ - def solve( self, - ops: list[tuple[int, int]], + ops: Sequence[tuple[int, int]], num_qubits: int, num_stages: int, - num_transfers: int | None = ..., - mind_ops_order: bool = ..., - shield_idle_qubits: bool = ..., - ) -> Result: + num_transfers: int | None = None, + mind_ops_order: bool = False, + shield_idle_qubits: bool = True, + ) -> NAStatePreparationSolver.Result: """Solve the neutral atom state preparation problem. - The solver generates an optimal sequence of neutral atom operations for a given - state preparation circuit. The circuit is given as a list of operations, where - each operation is a pair of qubits. The sequence is divided into stages. Each - stage is either a Rydberg stage or a transfer stage. In a Rydberg stage, - adjacent qubits in the entangling zone undergo an entangling gate. In a transfer - stage, atoms can be stored from AOD into SLM traps and loaded from SLM traps - into AOD. At the end of each stage, the atoms are shuttled to their next - position. The number of stages is specified by `num_stages`. The number of - transfers is fixed by `num_transfers` if given. If this parameter is not - specified, then the solver will determine the optimal number of transfers. The - parameter `mind_ops_order` specifies whether the order of the operations in the - circuit should be preserved. The parameter `shield_idle_qubits` specifies - whether idle qubits should be shielded from the entangling operations. + The solver generates an optimal sequence of neutral atom operations for a given state preparation circuit. + The circuit is given as a list of operations, where each operation is a pair of qubits. + The sequence is divided into stages. + Each stage is either a Rydberg stage or a transfer stage. + In a Rydberg stage, adjacent qubits in the entangling zone undergo an entangling gate. + In a transfer stage, atoms can be stored from AOD into SLM traps and loaded from SLM traps into AOD. + At the end of each stage, the atoms are shuttled to their next position. + The number of stages is specified by `num_stages`. + The number of transfers is fixed by `num_transfers` if given. + If this parameter is not specified, then the solver will determine the optimal number of transfers. + The parameter `mind_ops_order` specifies whether the order of the operations in the circuit should be preserved. + The parameter `shield_idle_qubits` specifies whether idle qubits should be shielded from the entangling operations. Note: - To retrieve the list of qubit pairs from a quantum circuit, use the function - :func:`get_ops_for_solver`. + To retrieve the list of qubit pairs from a quantum circuit, use the function :func:`get_ops_for_solver`. - - The returned solver's result can either be directly exported to the JSON format - by calling the method :func:`json` on the result object or the result object - can be passed to the function :func:`generate_code` to generate code - consisting of neutral atom operations. + The returned solver's result can either be directly exported to the JSON format by calling the method :func:`json` on the result object or the result object can be passed to the function :func:`generate_code` to generate code consisting of neutral atom operations. Args: - ops: is the list of operations in the circuit - num_qubits: is the number of qubits in the circuit - num_stages: is the number of stages in the sequence - num_transfers: (optional) is the number of transfers in the sequence - mind_ops_order: is True if the order of the operations should be - preserved - shield_idle_qubits: is True if idle qubits should be shielded + ops: The list of operations in the circuit + num_qubits: The number of qubits in the circuit + num_stages: The number of stages in the sequence + num_transfers: The number of transfers in the sequence. Can be `None` + mind_ops_order: Whether the order of the operations should be preserved + shield_idle_qubits: Whether idle qubits should be shielded Returns: - the result of the solver + The result of the solver Raises: - ValueError: if one of the numeral parameters is invalid, e.g., is a - negative value + ValueError: if one of the numeral parameters is invalid, e.g., is a negative value """ -def get_ops_for_solver( - qc: QuantumComputation, - operation_type: str, - num_controls: int, - quiet: bool = ..., -) -> list[tuple[int, int]]: - """Extract entangling operations as list of qubit pairs from the circuit. + class Result: + """The result of a :class:`~.NAStatePreparationSolver`.""" - Note: - This function can only extract qubit pairs of two-qubit operations. - I.e., the operands of the operation plus the controls must be equal to two. + def __init__(self) -> None: + """Create a result object.""" + + def json(self) -> dict[str, Any]: + """Returns the result as JSON-style dictionary. + + Returns: + The result as a JSON-style dictionary + """ + +def generate_code( + qc: mqt.core.ir.QuantumComputation, + result: NAStatePreparationSolver.Result, + min_atom_dist: int = 1, + no_interaction_radius: int = 10, + zone_dist: int = 24, +) -> str: + """Generate code for the given circuit using the solver's result. + + Some parameters of the abstraction from the 2D grid used for the solver must be provided again. Args: - qc: is the quantum circuit - operation_type: is the type of operation to extract, e.g., "z" for CZ - gates - num_controls: is the number of controls the operation acts on, e.g., 1 - for CZ gates - quiet: if True, suppresses warning when the circuit contains operations - other than the specified operation type + qc: The quantum circuit + result: The result of the solver + min_atom_dist: The minimum distance between atoms + no_interaction_radius: The radius around an atom where no other atom can be placed during an entangling operation that should not interact with the atom + zone_dist: The distance between zones, i.e., the minimal distance between two atoms in different zones Returns: - list of qubit pairs + The generated code as a string Raises: - ValueError: if the circuit contains operations other than the specified - operation type and quiet is False - ValueError: if the operation has more than two operands including - controls + ValueError: If one of the numeral parameters is invalid, e.g., is a negative value """ -def generate_code( - qc: QuantumComputation, - result: NAStatePreparationSolver.Result, - min_atom_dist: int = ..., - no_interaction_radius: int = ..., - zone_dist: int = ..., -) -> str: - """Generate code for the given circuit using the solver's result. +def get_ops_for_solver( + qc: mqt.core.ir.QuantumComputation, operation_type: str = "Z", num_controls: int = 1, quiet: bool = True +) -> list[tuple[int, int]]: + """Extract entangling operations as list of qubit pairs from the circuit. - Some parameters of the abstraction from the 2D grid used for the solver - must be provided again. + Note: + This function can only extract qubit pairs of two-qubit operations. + I.e., the operands of the operation plus the controls must be equal to two. Args: - qc: is the quantum circuit - result: is the result of the solver - min_atom_dist: is the minimum distance between atoms - no_interaction_radius: is the radius around an atom where no other atom - can be placed during an entangling operation that should not interact with the - atom - zone_dist: is the distance between zones, i.e., the minimal distance - between two atoms in different zones + qc: The quantum circuit + operation_type: The type of operation to extract, e.g., "z" for CZ gates + num_controls: The number of controls the operation acts on, e.g., 1 for CZ gates + quiet: Whether to suppress warnings when the circuit contains operations other than the specified operation type + + Returns: + List of qubit pairs Raises: - ValueError: if one of the numeral parameters is invalid, e.g., is a - negative value + ValueError: If the circuit contains operations other than the specified operation type and quiet is False + ValueError: If the operation has more than two operands including controls """ diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 28c875283..b0fb618bf 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -6,234 +6,216 @@ # # Licensed under the MIT License -"""Python bindings module for MQT QMAP's Zoned Neutral Atom Compiler.""" +import enum -from enum import Enum - -from mqt.core.ir import QuantumComputation +import mqt.core.ir class ZonedNeutralAtomArchitecture: - """Class representing a Zoned Neutral Atom Architecture.""" + """Class representing a zoned neutral atom architecture.""" - @classmethod - def from_json_file(cls, filename: str) -> ZonedNeutralAtomArchitecture: + @staticmethod + def from_json_file(filename: str) -> ZonedNeutralAtomArchitecture: """Create an architecture from a JSON file. Args: - filename: is the path to the JSON file + filename: The path to the JSON file Returns: - the architecture + The architecture Raises: ValueError: if the file does not exist or is not a valid JSON file """ - @classmethod - def from_json_string(cls, json: str) -> ZonedNeutralAtomArchitecture: + + @staticmethod + def from_json_string(json: str) -> ZonedNeutralAtomArchitecture: """Create an architecture from a JSON string. Args: - json: is the JSON string + json: The JSON string Returns: - the architecture + The architecture Raises: - ValueError: if the string is not a valid JSON + ValueError: If the string is not a valid JSON string """ + def to_namachine_file(self, filename: str) -> None: """Write the architecture to a .namachine file. Args: - filename: is the path to the .namachine file + filename: The path to the .namachine file """ + def to_namachine_string(self) -> str: """Get the architecture as a .namachine string. Returns: - the architecture as a .namachine string + The architecture as a .namachine string """ -class PlacementMethod(Enum): +class PlacementMethod(enum.Enum): """Enumeration of the available placement methods for the heuristic placer.""" - astar = ... - """ - A-star algorithm - """ - ids = ... - """ - Iterative diving search - """ + astar = 0 + """A-star algorithm.""" + + ids = 1 + """Iterative diving search.""" -class RoutingMethod(Enum): +class RoutingMethod(enum.Enum): """Enumeration of the available routing methods for the independent set router.""" - strict = ... + strict = 0 """ - Strict routing, i.e., the relative order of atoms must be - maintained throughout a movement. + Strict routing, i.e., the relative order of atoms must be maintained throughout a movement. """ - relaxed = ... + + relaxed = 1 """ - Relaxed routing, i.e., the relative order of atoms may change - throughout a movement by applying offsets during pick-up and drop-off. + Relaxed routing, i.e., the relative order of atoms may change throughout a movement by applying offsets during pick-up and drop-off. """ class RoutingAgnosticCompiler: - """MQT QMAP's routing-agnostic Zoned Neutral Atom Compiler.""" + """Routing-agnostic zoned neutral atom compiler.""" def __init__( self, arch: ZonedNeutralAtomArchitecture, - log_level: str = ..., - max_filling_factor: float = ..., - use_window: bool = ..., - window_size: int = ..., - dynamic_placement: bool = ..., + log_level: str = "I", + max_filling_factor: float = 0.9, + use_window: bool = True, + window_size: int = 10, + dynamic_placement: bool = True, routing_method: RoutingMethod = ..., - prefer_split: float = ..., - warn_unsupported_gates: bool = ..., + prefer_split: float = 1.0, + warn_unsupported_gates: bool = True, ) -> None: """Create a routing-agnostic compiler for the given architecture and configurations. Args: - arch: is the zoned neutral atom architecture - log_level: is the log level for the compiler, possible values are - "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., - it sets the limit for the maximum number of entangling gates that are - scheduled in parallel - use_window: whether to use a window for the placer - window_size: the size of the window for the placer - dynamic_placement: whether to use dynamic placement for the placer - routing_method: is the routing method that should be used for the - independent set router - prefer_split: is the threshold factor for group merging decisions during routing. - warn_unsupported_gates: whether to warn about unsupported gates in the code - generator + arch: The zoned neutral atom architecture + log_level: The log level for the compiler, possible values are "debug"/"D", "info"/"I", "warning"/"W", "error"/"E", and "critical"/"C" + max_filling_factor: The maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel + use_window: Whether to use a window for the placer + window_size: The size of the window for the placer + dynamic_placement: Whether to use dynamic placement for the placer + routing_method: The routing method that should be used for the independent set router + prefer_split: The threshold factor for group merging decisions during routing. + warn_unsupported_gates: Whether to warn about unsupported gates in the code generator """ - @classmethod - def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAgnosticCompiler: + + @staticmethod + def from_json_string(arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAgnosticCompiler: """Create a compiler for the given architecture and with configurations from a JSON string. Args: - arch: is the zoned neutral atom architecture - json: is the JSON string + arch: The zoned neutral atom architecture + json: The JSON string Returns: - the initialized compiler + The initialized compiler Raises: - ValueError: if the string is not a valid JSON + ValueError: If the string is not a valid JSON string """ - def compile(self, qc: QuantumComputation) -> str: + + def compile(self, qc: mqt.core.ir.QuantumComputation) -> str: """Compile a quantum circuit for the zoned neutral atom architecture. Args: - qc: is the quantum circuit + qc: The quantum circuit Returns: - the compilations result as a string in the .naviz format. + The compilation result as a string in the .naviz format. """ + def stats(self) -> dict[str, float]: - """Get the statistics of the last compilation. + """Get the statistics of the last compilation as a JSON-style dictionary. Returns: - the statistics as a dictionary + The statistics as a JSON-style dictionary """ class RoutingAwareCompiler: - """MQT QMAP's routing-aware Zoned Neutral Atom Compiler.""" + """Routing-aware zoned neutral atom compiler.""" def __init__( self, arch: ZonedNeutralAtomArchitecture, - log_level: str = ..., - max_filling_factor: float = ..., - use_window: bool = ..., - window_min_width: int = ..., - window_ratio: float = ..., - window_share: float = ..., + log_level: str = "I", + max_filling_factor: float = 0.9, + use_window: bool = True, + window_min_width: int = 16, + window_ratio: float = 1.0, + window_share: float = 0.8, placement_method: PlacementMethod = ..., deepening_factor: float = ..., - deepening_value: float = ..., + deepening_value: float = 0.0, lookahead_factor: float = ..., - reuse_level: float = ..., - max_nodes: int = ..., - trials: int = ..., - queue_capacity: int = ..., + reuse_level: float = 5.0, + max_nodes: int = 10000000, + trials: int = 4, + queue_capacity: int = 100, routing_method: RoutingMethod = ..., - prefer_split: float = ..., - warn_unsupported_gates: bool = ..., + prefer_split: float = 1.0, + warn_unsupported_gates: bool = True, ) -> None: """Create a routing-aware compiler for the given architecture and configurations. Args: - arch: is the zoned neutral atom architecture - log_level: is the log level for the compiler, possible values are - "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, - i.e., it sets the limit for the maximum number of entangling gates that - are scheduled in parallel - use_window: is a flag whether to use a window for the placer - window_min_width: is the minimum width of the window for the placer - window_ratio: is the ratio between the height and the width of the window - window_share: is the share of free sites in the window in relation to the - number of atoms to be moved in this step - placement_method: is the placement method that should be used for the heuristic - placer - deepening_factor: controls the impact of the term in the heuristic of the - A* search that resembles the standard deviation of the differences - between the current and target sites of the atoms to be moved in every - orientation - deepening_value: is added to the sum of standard deviations before it is - multiplied with the number of unplaced nodes and :attr:`deepening_factor` - lookahead_factor: controls the lookahead's influence that considers the - distance of atoms to their interaction partner in the next layer - reuse_level: is the reuse level that corresponds to the estimated extra - fidelity loss due to the extra trap transfers when the atom is not - reused and instead moved to the storage zone and back to the - entanglement zone - max_nodes: is the maximum number of nodes that are considered in the A* - search. If this number is exceeded, the search is aborted and an error - is raised. In the current implementation, one node roughly consumes 120 - Byte. Hence, allowing 50,000,000 nodes results in memory consumption of - about 6 GB plus the size of the rest of the data structures. - trials: is the number of restarts during IDS. - queue_capacity: is the maximum capacity of the priority queue used during IDS. - routing_method: is the routing method that should be used for the - independent set router - prefer_split: is the threshold factor for group merging decisions during routing. - warn_unsupported_gates: is a flag whether to warn about unsupported gates - in the code generator + arch: The zoned neutral atom architecture + log_level: The log level for the compiler, possible values are "debug"/"D", "info"/"I", "warning"/"W", "error"/"E", and "critical"/"C" + max_filling_factor: The maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel + use_window: Whether to use a window for the placer + window_min_width: The minimum width of the window for the placer + window_ratio: The ratio between the height and the width of the window + window_share: The share of free sites in the window in relation to the number of atoms to be moved in this step + placement_method: The placement method that should be used for the heuristic placer + deepening_factor: Controls the impact of the term in the heuristic of the A* search that resembles the standard deviation of the differences between the current and target sites of the atoms to be moved in every orientation + deepening_value: Is added to the sum of standard deviations before it is multiplied with the number of unplaced nodes and :attr:`deepening_factor` + lookahead_factor: Controls the lookahead's influence that considers the distance of atoms to their interaction partner in the next layer + reuse_level: The reuse level that corresponds to the estimated extra fidelity loss due to the extra trap transfers when the atom is not reused and instead moved to the storage zone and back to the entanglement zone + max_nodes: The maximum number of nodes that are considered in the A* search. + If this number is exceeded, the search is aborted and an error is raised. + In the current implementation, one node roughly consumes 120 Byte. + Hence, allowing 50,000,000 nodes results in memory consumption of about 6 GB plus the size of the rest of the data structures. + trials: The number of restarts during IDS. + queue_capacity: The maximum capacity of the priority queue used during IDS. + routing_method: The routing method that should be used for the independent set router + prefer_split: The threshold factor for group merging decisions during routing. + warn_unsupported_gates: Whether to warn about unsupported gates in the code generator """ - @classmethod - def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAwareCompiler: + + @staticmethod + def from_json_string(arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAwareCompiler: """Create a compiler for the given architecture and configurations from a JSON string. Args: - arch: is the zoned neutral atom architecture - json: is the JSON string + arch: The zoned neutral atom architecture + json: The JSON string Returns: - the initialized compiler + The initialized compiler Raises: - ValueError: if the string is not a valid JSON + ValueError: If the string is not a valid JSON string """ - def compile(self, qc: QuantumComputation) -> str: + + def compile(self, qc: mqt.core.ir.QuantumComputation) -> str: """Compile a quantum circuit for the zoned neutral atom architecture. Args: - qc: is the quantum circuit + qc: The quantum circuit Returns: - the compilations result as a string in the .naviz format. + The compilation result as a string in the .naviz format. """ + def stats(self) -> dict[str, float]: """Get the statistics of the last compilation. Returns: - the statistics as a dictionary + The statistics as a dictionary """ diff --git a/python/mqt/qmap/plugins/qiskit/sc/__init__.py b/python/mqt/qmap/plugins/qiskit/sc/__init__.py index 7a6c4c9d7..9bf46328c 100644 --- a/python/mqt/qmap/plugins/qiskit/sc/__init__.py +++ b/python/mqt/qmap/plugins/qiskit/sc/__init__.py @@ -10,7 +10,7 @@ from __future__ import annotations -from .compile import compile # noqa: A004 +from .compile import compile_ from .import_backend import import_backend, import_target from .load_architecture import load_architecture from .load_calibration import load_calibration @@ -22,7 +22,7 @@ __all__ = [ "SubarchitectureOrder", - "compile", + "compile_", "ibm_guadalupe_subarchitectures", "import_backend", "import_target", diff --git a/python/mqt/qmap/plugins/qiskit/sc/compile.py b/python/mqt/qmap/plugins/qiskit/sc/compile.py index 9b9b14ef5..a4e00dafe 100644 --- a/python/mqt/qmap/plugins/qiskit/sc/compile.py +++ b/python/mqt/qmap/plugins/qiskit/sc/compile.py @@ -26,7 +26,7 @@ LookaheadHeuristic, Method, SwapReduction, - map, # noqa: A004 + map_, ) from .load_architecture import load_architecture from .load_calibration import load_calibration @@ -41,7 +41,7 @@ from ....visualization import SearchVisualizer -def compile( # noqa: A001 +def compile_( circ: CircuitInputType, arch: str | Arch | Architecture | BackendV2 | None, calibration: str | Target | None = None, @@ -60,7 +60,7 @@ def compile( # noqa: A001 commander_grouping: CommanderGrouping = CommanderGrouping.fixed3, swap_reduction: SwapReduction = SwapReduction.coupling_limit, swap_limit: int = 0, - include_WCNF: bool = False, # noqa: N803 + include_wcnf: bool = False, use_subsets: bool = True, subgraph: set[int] | None = None, pre_mapping_optimizations: bool = True, @@ -92,7 +92,7 @@ def compile( # noqa: A001 commander_grouping: The grouping strategy to use for the commander and bimander encoding. Defaults to :attr:`~CommanderGrouping.halves`. swap_reduction: The swap reduction strategy to use. Defaults to :attr:`~SwapReduction.coupling_limit`. swap_limit: Set a custom limit for max swaps per layer, for the increasing reduction strategy it sets the max swaps per layer. Defaults to 0. - include_WCNF: Include WCNF file in the results. Defaults to False. + include_wcnf: Include WCNF file in the results. Defaults to False. use_subsets: Use qubit subsets, or consider all available physical qubits at once. Defaults to True. subgraph: List of qubits to consider for mapping (in exact mapper), if None all qubits are considered. Defaults to None. pre_mapping_optimizations: Run pre-mapping optimizations. Defaults to True. @@ -137,7 +137,7 @@ def compile( # noqa: A001 config.commander_grouping = CommanderGrouping(commander_grouping) config.swap_reduction = SwapReduction(swap_reduction) config.swap_limit = swap_limit - config.include_WCNF = include_WCNF + config.include_wcnf = include_wcnf config.use_subsets = use_subsets config.subgraph = subgraph config.pre_mapping_optimizations = pre_mapping_optimizations @@ -157,5 +157,5 @@ def compile( # noqa: A001 config.lookahead_factor = lookahead_factor qc = load(circ) - qc_mapped, results = map(qc, architecture, config) + qc_mapped, results = map_(qc, architecture, config) return mqt_to_qiskit(qc_mapped, set_layout=True), results diff --git a/python/mqt/qmap/sc.pyi b/python/mqt/qmap/sc.pyi index 41d394085..fd0c5ac0d 100644 --- a/python/mqt/qmap/sc.pyi +++ b/python/mqt/qmap/sc.pyi @@ -6,258 +6,478 @@ # # Licensed under the MIT License -"""Python bindings for superconducting module.""" +import enum +from collections.abc import Sequence +from collections.abc import Set as AbstractSet +from typing import Any, overload -from enum import Enum -from typing import Any, ClassVar, overload +import mqt.core.ir -from mqt.core.ir import QuantumComputation +class Arch(enum.Enum): + IBM_QX4 = 0 + """5 qubit, directed bow tie layout""" -class Arch(Enum): - IBMQ_Bogota = ... - IBMQ_Casablanca = ... - IBMQ_London = ... - IBMQ_Tokyo = ... - IBMQ_Yorktown = ... - IBM_QX4 = ... - IBM_QX5 = ... - Rigetti_Agave = ... - Rigetti_Aspen = ... + IBM_QX5 = 1 + """16 qubit, directed ladder layout""" -class Architecture: - """Class representing device/backend information.""" - class Properties: - """Class representing properties of an architecture.""" + IBMQ_Yorktown = 2 + """5 qubit, undirected bow tie layout""" - name: str - num_qubits: int + IBMQ_London = 3 + """5 qubit, undirected T-shape layout""" - def __init__(self) -> None: ... - def get_calibration_date(self, qubit: int) -> str: ... - def get_frequency(self, qubit: int) -> float: ... - def get_readout_error(self, qubit: int) -> float: ... - def get_single_qubit_error(self, qubit: int, operation: str) -> float: ... - def get_t1(self, qubit: int) -> float: ... - def get_t2(self, qubit: int) -> float: ... - def get_two_qubit_error(self, control: int, target: int, operation: str = ...) -> float: ... - def json(self) -> dict[str, Any]: ... - def set_calibration_date(self, qubit: int, calibration_date: str) -> None: ... - def set_frequency(self, qubit: int, qubit_frequency: float) -> None: ... - def set_readout_error(self, qubit: int, readout_error_rate: float) -> None: ... - def set_single_qubit_error(self, qubit: int, operation: str, error_rate: float) -> None: ... - def set_t1(self, qubit: int, t1: float) -> None: ... - def set_t2(self, qubit: int, t2: float) -> None: ... - def set_two_qubit_error(self, control: int, target: int, error_rate: float, operation: str = ...) -> None: ... + IBMQ_Bogota = 4 + """5 qubit, undirected linear chain layout""" - coupling_map: set[tuple[int, int]] - name: str - num_qubits: int - properties: Properties + IBMQ_Casablanca = 5 + """7 qubit, undirected H-shape layout""" - @overload - def __init__(self) -> None: ... - @overload - def __init__(self, num_qubits: int, coupling_map: set[tuple[int, int]]) -> None: ... - @overload - def __init__(self, num_qubits: int, coupling_map: set[tuple[int, int]], properties: Properties) -> None: ... - @overload - def load_coupling_map(self, available_architecture: Arch) -> None: ... - @overload - def load_coupling_map(self, coupling_map_file: str) -> None: ... - @overload - def load_properties(self, properties: Properties) -> None: ... - @overload - def load_properties(self, properties: str) -> None: ... + IBMQ_Tokyo = 6 + """20 qubit, undirected brick-like layout""" -class CircuitInfo: - """Circuit information.""" - - cnots: int - direction_reverse: int - gates: int - layers: int - total_fidelity: float - total_log_fidelity: float - name: str - qubits: int - single_qubit_gates: int - swaps: int + Rigetti_Agave = 7 + """8 qubit, undirected ring layout""" - def __init__(self) -> None: ... + Rigetti_Aspen = 8 + """16 qubit, undirected dumbbell layout""" -class CommanderGrouping: - __members__: ClassVar[dict[CommanderGrouping, int]] = ... # read-only - fixed2: ClassVar[CommanderGrouping] = ... - fixed3: ClassVar[CommanderGrouping] = ... - halves: ClassVar[CommanderGrouping] = ... - logarithm: ClassVar[CommanderGrouping] = ... +class Method(enum.Enum): + heuristic = 2 - @overload - def __init__(self, value: int) -> None: ... - @overload - def __init__(self, arg0: str) -> None: ... - @overload - def __init__(self, arg0: CommanderGrouping) -> None: ... - def __eq__(self, other: object) -> bool: ... - def __getstate__(self) -> int: ... - def __hash__(self) -> int: ... - def __index__(self) -> int: ... - def __int__(self) -> int: ... - def __ne__(self, other: object) -> bool: ... - def __setstate__(self, state: int) -> None: ... - @property - def name(self) -> str: ... - @property - def value(self) -> int: ... + exact = 1 + +class InitialLayout(enum.Enum): + identity = 0 + + static = 1 + + dynamic = 2 + +class Heuristic(enum.Enum): + gate_count_max_distance = 0 + + gate_count_sum_distance = 1 + + gate_count_sum_distance_minus_shared_swaps = 2 + + gate_count_max_distance_or_sum_distance_minus_shared_swaps = 3 + + fidelity_best_location = 4 + +class LookaheadHeuristic(enum.Enum): + none = 0 + + gate_count_max_distance = 1 + + gate_count_sum_distance = 2 + +class Layering(enum.Enum): + individual_gates = 0 + + disjoint_qubits = 1 + + odd_gates = 2 + + qubit_triangle = 3 + + disjoint_2q_blocks = 4 + +class EarlyTermination(enum.Enum): + none = 0 + + expanded_nodes = 1 + + expanded_nodes_after_first_solution = 2 + + expanded_nodes_after_current_optimal_solution = 3 + + solution_nodes = 4 + + solution_nodes_after_current_optimal_solution = 5 + +class Encoding(enum.Enum): + naive = 0 + + commander = 1 + + bimander = 2 + +class CommanderGrouping(enum.Enum): + fixed2 = 1 + + fixed3 = 2 + + halves = 0 + + logarithm = 3 + +class SwapReduction(enum.Enum): + none = 0 + + coupling_limit = 1 + + custom = 2 + + increasing = 3 class Configuration: """Class representing the configuration for the mapping.""" - add_measurements_to_mapped_circuit: bool - add_barriers_between_layers: bool - heuristic: Heuristic - commander_grouping: CommanderGrouping - enable_limits: bool - encoding: Encoding - first_lookahead_factor: float - include_WCNF: bool # noqa: N815 - initial_layout: InitialLayout - iterative_bidirectional_routing: bool - iterative_bidirectional_routing_passes: int - layering: Layering - automatic_layer_splits: bool - automatic_layer_splits_node_limit: int - early_termination: EarlyTermination - early_termination_limit: int - lookahead_heuristic: LookaheadHeuristic - lookahead_factor: float - lookaheads: int - method: Method - post_mapping_optimizations: bool - pre_mapping_optimizations: bool - subgraph: set[int] - swap_limit: int - swap_reduction: SwapReduction - timeout: int - use_subsets: bool - verbose: bool - debug: bool - data_logging_path: str + def __init__(self) -> None: ... + @property + def method(self) -> Method: ... + @method.setter + def method(self, arg: Method, /) -> None: ... + @property + def heuristic(self) -> Heuristic: ... + @heuristic.setter + def heuristic(self, arg: Heuristic, /) -> None: ... + @property + def verbose(self) -> bool: ... + @verbose.setter + def verbose(self, arg: bool, /) -> None: ... + @property + def debug(self) -> bool: ... + @debug.setter + def debug(self, arg: bool, /) -> None: ... + @property + def data_logging_path(self) -> str: ... + @data_logging_path.setter + def data_logging_path(self, arg: str, /) -> None: ... + @property + def layering(self) -> Layering: ... + @layering.setter + def layering(self, arg: Layering, /) -> None: ... + @property + def automatic_layer_splits(self) -> bool: ... + @automatic_layer_splits.setter + def automatic_layer_splits(self, arg: bool, /) -> None: ... + @property + def automatic_layer_splits_node_limit(self) -> int: ... + @automatic_layer_splits_node_limit.setter + def automatic_layer_splits_node_limit(self, arg: int, /) -> None: ... + @property + def early_termination(self) -> EarlyTermination: ... + @early_termination.setter + def early_termination(self, arg: EarlyTermination, /) -> None: ... + @property + def early_termination_limit(self) -> int: ... + @early_termination_limit.setter + def early_termination_limit(self, arg: int, /) -> None: ... + @property + def initial_layout(self) -> InitialLayout: ... + @initial_layout.setter + def initial_layout(self, arg: InitialLayout, /) -> None: ... + @property + def iterative_bidirectional_routing(self) -> bool: ... + @iterative_bidirectional_routing.setter + def iterative_bidirectional_routing(self, arg: bool, /) -> None: ... + @property + def iterative_bidirectional_routing_passes(self) -> int: ... + @iterative_bidirectional_routing_passes.setter + def iterative_bidirectional_routing_passes(self, arg: int, /) -> None: ... + @property + def lookahead_heuristic(self) -> LookaheadHeuristic: ... + @lookahead_heuristic.setter + def lookahead_heuristic(self, arg: LookaheadHeuristic, /) -> None: ... + @property + def lookaheads(self) -> int: ... + @lookaheads.setter + def lookaheads(self, arg: int, /) -> None: ... + @property + def first_lookahead_factor(self) -> float: ... + @first_lookahead_factor.setter + def first_lookahead_factor(self, arg: float, /) -> None: ... + @property + def lookahead_factor(self) -> float: ... + @lookahead_factor.setter + def lookahead_factor(self, arg: float, /) -> None: ... + @property + def timeout(self) -> int: ... + @timeout.setter + def timeout(self, arg: int, /) -> None: ... + @property + def encoding(self) -> Encoding: ... + @encoding.setter + def encoding(self, arg: Encoding, /) -> None: ... + @property + def commander_grouping(self) -> CommanderGrouping: ... + @commander_grouping.setter + def commander_grouping(self, arg: CommanderGrouping, /) -> None: ... + @property + def use_subsets(self) -> bool: ... + @use_subsets.setter + def use_subsets(self, arg: bool, /) -> None: ... + @property + def include_wcnf(self) -> bool: ... + @include_wcnf.setter + def include_wcnf(self, arg: bool, /) -> None: ... + @property + def enable_limits(self) -> bool: ... + @enable_limits.setter + def enable_limits(self, arg: bool, /) -> None: ... + @property + def swap_reduction(self) -> SwapReduction: ... + @swap_reduction.setter + def swap_reduction(self, arg: SwapReduction, /) -> None: ... + @property + def swap_limit(self) -> int: ... + @swap_limit.setter + def swap_limit(self, arg: int, /) -> None: ... + @property + def subgraph(self) -> set[int]: ... + @subgraph.setter + def subgraph(self, arg: AbstractSet[int], /) -> None: ... + @property + def pre_mapping_optimizations(self) -> bool: ... + @pre_mapping_optimizations.setter + def pre_mapping_optimizations(self, arg: bool, /) -> None: ... + @property + def post_mapping_optimizations(self) -> bool: ... + @post_mapping_optimizations.setter + def post_mapping_optimizations(self, arg: bool, /) -> None: ... + @property + def add_measurements_to_mapped_circuit(self) -> bool: ... + @add_measurements_to_mapped_circuit.setter + def add_measurements_to_mapped_circuit(self, arg: bool, /) -> None: ... + @property + def add_barriers_between_layers(self) -> bool: ... + @add_barriers_between_layers.setter + def add_barriers_between_layers(self, arg: bool, /) -> None: ... + def json(self) -> dict[str, Any]: ... + +class MappingResults: + """Class representing the results of a mapping.""" def __init__(self) -> None: ... + @property + def input_(self) -> CircuitInfo: ... + @input_.setter + def input_(self, arg: CircuitInfo, /) -> None: ... + @property + def output(self) -> CircuitInfo: ... + @output.setter + def output(self, arg: CircuitInfo, /) -> None: ... + @property + def configuration(self) -> Configuration: ... + @configuration.setter + def configuration(self, arg: Configuration, /) -> None: ... + @property + def time(self) -> float: ... + @time.setter + def time(self, arg: float, /) -> None: ... + @property + def timeout(self) -> bool: ... + @timeout.setter + def timeout(self, arg: bool, /) -> None: ... + @property + def mapped_circuit(self) -> str: ... + @mapped_circuit.setter + def mapped_circuit(self, arg: str, /) -> None: ... + @property + def heuristic_benchmark(self) -> HeuristicBenchmarkInfo: ... + @heuristic_benchmark.setter + def heuristic_benchmark(self, arg: HeuristicBenchmarkInfo, /) -> None: ... + @property + def layer_heuristic_benchmark(self) -> list[LayerHeuristicBenchmarkInfo]: ... + @layer_heuristic_benchmark.setter + def layer_heuristic_benchmark(self, arg: Sequence[LayerHeuristicBenchmarkInfo], /) -> None: ... + @property + def wcnf(self) -> str: ... + @wcnf.setter + def wcnf(self, arg: str, /) -> None: ... def json(self) -> dict[str, Any]: ... -class EarlyTermination: - __members__: ClassVar[dict[EarlyTermination, int]] = ... # read-only - none: ClassVar[EarlyTermination] = ... - expanded_nodes: ClassVar[EarlyTermination] = ... - expanded_nodes_after_first_solution: ClassVar[EarlyTermination] = ... - expanded_nodes_after_current_optimal_solution: ClassVar[EarlyTermination] = ... - solution_nodes: ClassVar[EarlyTermination] = ... - solution_nodes_after_current_optimal_solution: ClassVar[EarlyTermination] = ... +class CircuitInfo: + """Class containing circuit information.""" - @overload - def __init__(self, value: int) -> None: ... - @overload - def __init__(self, arg0: str) -> None: ... - @overload - def __init__(self, arg0: EarlyTermination) -> None: ... - def __eq__(self, other: object) -> bool: ... - def __getstate__(self) -> int: ... - def __hash__(self) -> int: ... - def __index__(self) -> int: ... - def __int__(self) -> int: ... - def __ne__(self, other: object) -> bool: ... - def __setstate__(self, state: int) -> None: ... + def __init__(self) -> None: ... @property def name(self) -> str: ... + @name.setter + def name(self, arg: str, /) -> None: ... + @property + def qubits(self) -> int: ... + @qubits.setter + def qubits(self, arg: int, /) -> None: ... + @property + def gates(self) -> int: ... + @gates.setter + def gates(self, arg: int, /) -> None: ... + @property + def single_qubit_gates(self) -> int: ... + @single_qubit_gates.setter + def single_qubit_gates(self, arg: int, /) -> None: ... + @property + def cnots(self) -> int: ... + @cnots.setter + def cnots(self, arg: int, /) -> None: ... + @property + def layers(self) -> int: ... + @layers.setter + def layers(self, arg: int, /) -> None: ... + @property + def total_fidelity(self) -> float: ... + @total_fidelity.setter + def total_fidelity(self, arg: float, /) -> None: ... @property - def value(self) -> int: ... - -class Encoding(Enum): - bimander = ... - commander = ... - naive = ... - -class InitialLayout(Enum): - dynamic = ... - identity = ... - static = ... - -class Heuristic(Enum): - gate_count_max_distance = ... - gate_count_sum_distance = ... - gate_count_sum_distance_minus_shared_swaps = ... - gate_count_max_distance_or_sum_distance_minus_shared_swaps = ... - fidelity_best_location = ... - -class LookaheadHeuristic(Enum): - none = ... - gate_count_max_distance = ... - gate_count_sum_distance = ... - -class Layering(Enum): - disjoint_qubits = ... - individual_gates = ... - odd_gates = ... - qubit_triangle = ... - disjoint_2q_blocks = ... + def total_log_fidelity(self) -> float: ... + @total_log_fidelity.setter + def total_log_fidelity(self, arg: float, /) -> None: ... + @property + def swaps(self) -> int: ... + @swaps.setter + def swaps(self, arg: int, /) -> None: ... + @property + def direction_reverse(self) -> int: ... + @direction_reverse.setter + def direction_reverse(self, arg: int, /) -> None: ... class HeuristicBenchmarkInfo: - expanded_nodes: int - generated_nodes: int - seconds_per_node: float - average_branching_factor: float - effective_branching_factor: float + """Heuristic benchmark information.""" def __init__(self) -> None: ... + @property + def expanded_nodes(self) -> int: ... + @expanded_nodes.setter + def expanded_nodes(self, arg: int, /) -> None: ... + @property + def generated_nodes(self) -> int: ... + @generated_nodes.setter + def generated_nodes(self, arg: int, /) -> None: ... + @property + def time_per_node(self) -> float: ... + @time_per_node.setter + def time_per_node(self, arg: float, /) -> None: ... + @property + def average_branching_factor(self) -> float: ... + @average_branching_factor.setter + def average_branching_factor(self, arg: float, /) -> None: ... + @property + def effective_branching_factor(self) -> float: ... + @effective_branching_factor.setter + def effective_branching_factor(self, arg: float, /) -> None: ... def json(self) -> dict[str, Any]: ... class LayerHeuristicBenchmarkInfo: - expanded_nodes: int - generated_nodes: int - expanded_nodes_after_first_solution: int - expanded_nodes_after_optimal_solution: int - solution_nodes: int - solution_nodes_after_optimal_solution: int - solution_depth: int - seconds_per_node: float - average_branching_factor: float - effective_branching_factor: float - early_termination: bool + """Heuristic benchmark information.""" def __init__(self) -> None: ... + @property + def expanded_nodes(self) -> int: ... + @expanded_nodes.setter + def expanded_nodes(self, arg: int, /) -> None: ... + @property + def generated_nodes(self) -> int: ... + @generated_nodes.setter + def generated_nodes(self, arg: int, /) -> None: ... + @property + def expanded_nodes_after_first_solution(self) -> int: ... + @expanded_nodes_after_first_solution.setter + def expanded_nodes_after_first_solution(self, arg: int, /) -> None: ... + @property + def expanded_nodes_after_optimal_solution(self) -> int: ... + @expanded_nodes_after_optimal_solution.setter + def expanded_nodes_after_optimal_solution(self, arg: int, /) -> None: ... + @property + def solution_nodes(self) -> int: ... + @solution_nodes.setter + def solution_nodes(self, arg: int, /) -> None: ... + @property + def solution_nodes_after_optimal_solution(self) -> int: ... + @solution_nodes_after_optimal_solution.setter + def solution_nodes_after_optimal_solution(self, arg: int, /) -> None: ... + @property + def solution_depth(self) -> int: ... + @solution_depth.setter + def solution_depth(self, arg: int, /) -> None: ... + @property + def time_per_node(self) -> float: ... + @time_per_node.setter + def time_per_node(self, arg: float, /) -> None: ... + @property + def average_branching_factor(self) -> float: ... + @average_branching_factor.setter + def average_branching_factor(self, arg: float, /) -> None: ... + @property + def effective_branching_factor(self) -> float: ... + @effective_branching_factor.setter + def effective_branching_factor(self, arg: float, /) -> None: ... + @property + def early_termination(self) -> bool: ... + @early_termination.setter + def early_termination(self, arg: bool, /) -> None: ... def json(self) -> dict[str, Any]: ... -class MappingResults: - """Class representing the results of a mapping.""" - - configuration: Configuration - input: CircuitInfo - mapped_circuit: str - output: CircuitInfo - time: float - timeout: bool - wcnf: str - heuristic_benchmark: HeuristicBenchmarkInfo - layer_heuristic_benchmark: LayerHeuristicBenchmarkInfo +class Architecture: + """Class representing device/backend information.""" + @overload def __init__(self) -> None: ... - def json(self) -> dict[str, Any]: ... + @overload + def __init__(self, num_qubits: int, coupling_map: AbstractSet[tuple[int, int]]) -> None: ... + @overload + def __init__( + self, num_qubits: int, coupling_map: AbstractSet[tuple[int, int]], properties: Architecture.Properties + ) -> None: ... -class Method(Enum): - exact = ... - heuristic = ... + class Properties: + """Class representing properties of an architecture.""" + + def __init__(self) -> None: ... + @property + def name(self) -> str: ... + @name.setter + def name(self, arg: str, /) -> None: ... + @property + def num_qubits(self) -> int: ... + @num_qubits.setter + def num_qubits(self, arg: int, /) -> None: ... + def get_single_qubit_error(self, qubit: int, operation: str) -> float: ... + def set_single_qubit_error(self, qubit: int, operation: str, error_rate: float) -> None: ... + def get_two_qubit_error(self, control: int, target: int, operation: str = "cx") -> float: ... + def set_two_qubit_error(self, control: int, target: int, error_rate: float, operation: str = "cx") -> None: ... + def get_readout_error(self, qubit: int) -> float: ... + def set_readout_error(self, qubit: int, readout_error_rate: float) -> None: ... + def get_t1(self, qubit: int) -> float: ... + def set_t1(self, qubit: int, t1: float) -> None: ... + def get_t2(self, qubit: int) -> float: ... + def set_t2(self, qubit: int, t2: float) -> None: ... + def get_frequency(self, qubit: int) -> float: ... + def set_frequency(self, qubit: int, qubit_frequency: float) -> None: ... + def get_calibration_date(self, qubit: int) -> str: ... + def set_calibration_date(self, qubit: int, calibration_date: str) -> None: ... + def json(self) -> dict[str, Any]: + """Returns a JSON-style dictionary of all the information present in the :class:`.Properties`.""" -class SwapReduction(Enum): - coupling_limit = ... - custom = ... - increasing = ... - none = ... + @property + def name(self) -> str: ... + @name.setter + def name(self, arg: str, /) -> None: ... + @property + def num_qubits(self) -> int: ... + @num_qubits.setter + def num_qubits(self, arg: int, /) -> None: ... + @property + def coupling_map(self) -> set[tuple[int, int]]: ... + @coupling_map.setter + def coupling_map(self, arg: AbstractSet[tuple[int, int]], /) -> None: ... + @property + def properties(self) -> Architecture.Properties: ... + @properties.setter + def properties(self, arg: Architecture.Properties, /) -> None: ... + @overload + def load_coupling_map(self, available_architecture: Arch) -> None: ... + @overload + def load_coupling_map(self, coupling_map_file: str) -> None: ... + @overload + def load_properties(self, properties: Architecture.Properties) -> None: ... + @overload + def load_properties(self, properties: str) -> None: ... -def map( # noqa: A001 - circ: QuantumComputation, arch: Architecture, config: Configuration -) -> tuple[QuantumComputation, MappingResults]: +def map_( + circ: mqt.core.ir.QuantumComputation, arch: Architecture, config: Configuration +) -> tuple[mqt.core.ir.QuantumComputation, MappingResults]: """Map a quantum circuit to an architecture. Args: diff --git a/test/python/sc/test_compile.py b/test/python/sc/test_compile.py index 955723efe..71c1d21d5 100644 --- a/test/python/sc/test_compile.py +++ b/test/python/sc/test_compile.py @@ -17,7 +17,7 @@ from mqt.qcec import verify from qiskit import QuantumCircuit -from mqt.qmap.plugins.qiskit.sc import compile # noqa: A004 +from mqt.qmap.plugins.qiskit.sc import compile_ from mqt.qmap.sc import ( Arch, Architecture, @@ -47,7 +47,7 @@ def example_circuit() -> QuantumCircuit: def test_either_arch_or_calibration(example_circuit: QuantumCircuit) -> None: """Test that either arch or calibration must be provided.""" with pytest.raises(ValueError, match="Either arch or calibration must be specified"): - compile(example_circuit, arch=None, calibration=None) + compile_(example_circuit, arch=None, calibration=None) # test that all available architecture enumerations can be properly used @@ -66,7 +66,7 @@ def test_either_arch_or_calibration(example_circuit: QuantumCircuit) -> None: ) def test_available_architectures(example_circuit: QuantumCircuit, arch: Arch) -> None: """Test that the available architecture enums can be properly used.""" - example_circuit_mapped, results = compile(example_circuit, arch=arch) + example_circuit_mapped, results = compile_(example_circuit, arch=arch) assert results.timeout is False result = verify(example_circuit, example_circuit_mapped) @@ -78,7 +78,7 @@ def test_architecture_from_file(example_circuit: QuantumCircuit) -> None: with Path("test_architecture.arch").open("w+", encoding=locale.getpreferredencoding(False)) as f: f.write("3\n0 1\n0 2\n1 2\n") - example_circuit_mapped, results = compile(example_circuit, arch="test_architecture.arch") + example_circuit_mapped, results = compile_(example_circuit, arch="test_architecture.arch") assert results.timeout is False result = verify(example_circuit, example_circuit_mapped) @@ -88,7 +88,7 @@ def test_architecture_from_file(example_circuit: QuantumCircuit) -> None: def test_architecture_from_python(example_circuit: QuantumCircuit) -> None: """Test that architectures from python can be properly used.""" arch = Architecture(3, {(0, 1), (0, 2), (1, 2)}) - example_circuit_mapped, results = compile(example_circuit, arch=arch) + example_circuit_mapped, results = compile_(example_circuit, arch=arch) assert results.timeout is False result = verify(example_circuit, example_circuit_mapped) @@ -103,7 +103,7 @@ def test_calibration_from_file(example_circuit: QuantumCircuit) -> None: f.write('Q1,0,0,0,1e-2,1e-4,"1_2: 1e-2"\n') f.write("Q2,0,0,0,1e-2,1e-4, \n") - example_circuit_mapped, results = compile(example_circuit, arch=None, calibration="test_calibration.cal") + example_circuit_mapped, results = compile_(example_circuit, arch=None, calibration="test_calibration.cal") assert results.timeout is False result = verify(example_circuit, example_circuit_mapped) @@ -121,14 +121,14 @@ def test_parameters(example_circuit: QuantumCircuit) -> None: properties.set_two_qubit_error(1, 2, 0.02, "cx") properties.set_two_qubit_error(2, 1, 0.02, "cx") arch = Architecture(3, {(0, 1), (1, 0), (1, 2), (2, 1)}, properties) - _, results = compile( + _, results = compile_( example_circuit, arch=arch, method=Method.exact, encoding=Encoding.commander, commander_grouping=CommanderGrouping.fixed3, swap_reduction=SwapReduction.coupling_limit, - include_WCNF=False, + include_wcnf=False, use_subsets=True, subgraph=None, add_measurements_to_mapped_circuit=True, @@ -137,13 +137,13 @@ def test_parameters(example_circuit: QuantumCircuit) -> None: assert results.configuration.encoding == Encoding.commander assert results.configuration.commander_grouping == CommanderGrouping.fixed3 assert results.configuration.swap_reduction == SwapReduction.coupling_limit - assert results.configuration.include_WCNF is False + assert results.configuration.include_wcnf is False assert results.configuration.use_subsets is True assert results.configuration.subgraph == set() assert results.configuration.add_measurements_to_mapped_circuit is True with SearchVisualizer() as visualizer: - _, results = compile( + _, results = compile_( example_circuit, arch=arch, method=Method.heuristic, @@ -178,7 +178,7 @@ def test_parameters(example_circuit: QuantumCircuit) -> None: assert results.configuration.debug is True assert results.configuration.data_logging_path == visualizer.data_logging_path - _, results = compile( + _, results = compile_( example_circuit, arch=arch, method=Method.heuristic, diff --git a/test/python/sc/test_exact_mapper.py b/test/python/sc/test_exact_mapper.py index 47bdb744a..6cfab0a87 100644 --- a/test/python/sc/test_exact_mapper.py +++ b/test/python/sc/test_exact_mapper.py @@ -15,7 +15,7 @@ from qiskit import QuantumCircuit from qiskit.providers.fake_provider import GenericBackendV2 -from mqt.qmap.plugins.qiskit.sc import compile # noqa: A004 +from mqt.qmap.plugins.qiskit.sc import compile_ from mqt.qmap.sc import Method @@ -33,7 +33,7 @@ def test_exact_no_swaps_trivial_layout(backend: GenericBackendV2) -> None: qc.cx(1, 2) qc.measure_all() - qc_mapped, results = compile(qc, arch=backend, method=Method.exact) + qc_mapped, results = compile_(qc, arch=backend, method=Method.exact) assert results.timeout is False assert results.output.swaps == 0 @@ -50,7 +50,7 @@ def test_exact_no_swaps_non_trivial_layout(backend: GenericBackendV2) -> None: qc.cx(0, 3) qc.measure_all() - qc_mapped, results = compile(qc, arch=backend, method=Method.exact) + qc_mapped, results = compile_(qc, arch=backend, method=Method.exact) assert results.timeout is False assert results.output.swaps == 0 @@ -68,7 +68,7 @@ def test_exact_non_trivial_swaps(backend: GenericBackendV2) -> None: qc.cx(2, 0) qc.measure_all() - qc_mapped, results = compile(qc, arch=backend, method=Method.exact) + qc_mapped, results = compile_(qc, arch=backend, method=Method.exact) assert results.timeout is False assert results.output.swaps == 1 diff --git a/test/python/sc/test_heuristic_mapper.py b/test/python/sc/test_heuristic_mapper.py index 14de54510..e79ca6d83 100644 --- a/test/python/sc/test_heuristic_mapper.py +++ b/test/python/sc/test_heuristic_mapper.py @@ -15,7 +15,7 @@ from qiskit import QuantumCircuit from qiskit.providers.fake_provider import GenericBackendV2 -from mqt.qmap.plugins.qiskit.sc import compile # noqa: A004 +from mqt.qmap.plugins.qiskit.sc import compile_ @pytest.fixture @@ -32,7 +32,7 @@ def test_heuristic_no_swaps_trivial_layout(backend: GenericBackendV2) -> None: qc.cx(1, 2) qc.measure_all() - qc_mapped, results = compile(qc, arch=backend) + qc_mapped, results = compile_(qc, arch=backend) assert results.timeout is False # assert results.output.swaps == 0 @@ -49,7 +49,7 @@ def test_heuristic_no_swaps_non_trivial_layout(backend: GenericBackendV2) -> Non qc.cx(0, 3) qc.measure_all() - qc_mapped, results = compile(qc, arch=backend) + qc_mapped, results = compile_(qc, arch=backend) assert results.timeout is False # assert results.output.swaps == 0 @@ -67,7 +67,7 @@ def test_heuristic_non_trivial_swaps(backend: GenericBackendV2) -> None: qc.cx(2, 0) qc.measure_all() - qc_mapped, results = compile(qc, arch=backend) + qc_mapped, results = compile_(qc, arch=backend) assert results.timeout is False assert results.output.swaps == 1 diff --git a/test/python/sc/test_qiskit_backend_imports.py b/test/python/sc/test_qiskit_backend_imports.py index ec0b9cb18..79e9df22c 100644 --- a/test/python/sc/test_qiskit_backend_imports.py +++ b/test/python/sc/test_qiskit_backend_imports.py @@ -15,7 +15,7 @@ from qiskit import QuantumCircuit from qiskit.providers.fake_provider import GenericBackendV2 -from mqt.qmap.plugins.qiskit.sc import compile # noqa: A004 +from mqt.qmap.plugins.qiskit.sc import compile_ @pytest.fixture @@ -37,13 +37,13 @@ def backend() -> GenericBackendV2: def test_backend_v2(example_circuit: QuantumCircuit, backend: GenericBackendV2) -> None: """Test that circuits can be mapped to Qiskit BackendV1 instances providing the old basis_gates.""" - qc, results = compile(example_circuit, arch=backend) + qc, results = compile_(example_circuit, arch=backend) assert results.timeout is False assert verify(example_circuit, qc).considered_equivalent() def test_architecture_from_v2_target(example_circuit: QuantumCircuit, backend: GenericBackendV2) -> None: """Test that circuits can be mapped by simply providing the target (the BackendV2 way).""" - qc, results = compile(example_circuit, arch=None, calibration=backend.target) + qc, results = compile_(example_circuit, arch=None, calibration=backend.target) assert results.timeout is False assert verify(example_circuit, qc).considered_equivalent()