diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fb628de..e7fcf8bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ ### Added +- ✨ Add iterative diving search as a more efficient placement heuristic method ([#862]) ([**@ystade**]) - ✨ Add relaxed routing method to the zoned neutral atom compiler ([#859]) ([**@ystade**]) ### Changed @@ -173,6 +174,7 @@ _πŸ“š Refer to the [GitHub Release Notes] for previous changelogs._ [#874]: https://github.com/munich-quantum-toolkit/qmap/pull/874 +[#862]: https://github.com/munich-quantum-toolkit/qmap/pull/862 [#859]: https://github.com/munich-quantum-toolkit/qmap/pull/859 [#848]: https://github.com/munich-quantum-toolkit/qmap/pull/848 [#847]: https://github.com/munich-quantum-toolkit/qmap/pull/847 diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 0b53e3d1f..86ba2bdbc 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -13,7 +13,7 @@ #include "na/zoned/Compiler.hpp" #include "na/zoned/code_generator/CodeGenerator.hpp" #include "na/zoned/layout_synthesizer/PlaceAndRouteSynthesizer.hpp" -#include "na/zoned/layout_synthesizer/placer/AStarPlacer.hpp" +#include "na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp" #include "na/zoned/layout_synthesizer/placer/VertexMatchingPlacer.hpp" #include "na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp" @@ -53,6 +53,16 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { return self.exportNAVizMachine(); }); + //===--------------------------------------------------------------------===// + // Placement Method Enum + //===--------------------------------------------------------------------===// + py::native_enum( + m, "PlacementMethod", "enum.Enum") + .value("astar", na::zoned::HeuristicPlacer::Config::Method::ASTAR) + .value("ids", na::zoned::HeuristicPlacer::Config::Method::IDS) + .export_values() + .finalize(); + //===--------------------------------------------------------------------===// // Routing Method Enum //===--------------------------------------------------------------------===// @@ -140,37 +150,44 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { { const na::zoned::RoutingAwareCompiler::Config defaultConfig; routingAwareCompiler.def( - py::init([](const na::zoned::Architecture& arch, - const std::string& logLevel, const double maxFillingFactor, - const bool useWindow, const size_t windowMinWidth, - const double windowRatio, const double windowShare, - const float deepeningFactor, const float deepeningValue, - const float lookaheadFactor, const float reuseLevel, - const size_t maxNodes, - const na::zoned::IndependentSetRouter::Config::Method - routingMethod, - const double preferSplit, const bool warnUnsupportedGates) - -> na::zoned::RoutingAwareCompiler { - na::zoned::RoutingAwareCompiler::Config config; - config.logLevel = spdlog::level::from_str(logLevel); - config.schedulerConfig.maxFillingFactor = maxFillingFactor; - config.layoutSynthesizerConfig.placerConfig = { - .useWindow = useWindow, - .windowMinWidth = windowMinWidth, - .windowRatio = windowRatio, - .windowShare = windowShare, - .deepeningFactor = deepeningFactor, - .deepeningValue = deepeningValue, - .lookaheadFactor = lookaheadFactor, - .reuseLevel = reuseLevel, - .maxNodes = maxNodes, - }; - config.layoutSynthesizerConfig.routerConfig = { - .method = routingMethod, .preferSplit = preferSplit}; - config.codeGeneratorConfig = {.warnUnsupportedGates = - warnUnsupportedGates}; - return {arch, config}; - }), + py::init( + [](const na::zoned::Architecture& arch, const std::string& logLevel, + const double maxFillingFactor, const bool useWindow, + const size_t windowMinWidth, const double windowRatio, + const double windowShare, + const na::zoned::HeuristicPlacer::Config::Method placementMethod, + const float deepeningFactor, const float deepeningValue, + const float lookaheadFactor, const float reuseLevel, + const size_t maxNodes, const size_t trials, + const size_t queueCapacity, + const na::zoned::IndependentSetRouter::Config::Method + routingMethod, + const double preferSplit, const bool warnUnsupportedGates) + -> na::zoned::RoutingAwareCompiler { + na::zoned::RoutingAwareCompiler::Config config; + config.logLevel = spdlog::level::from_str(logLevel); + config.schedulerConfig.maxFillingFactor = maxFillingFactor; + + config.layoutSynthesizerConfig.placerConfig = { + .useWindow = useWindow, + .windowMinWidth = windowMinWidth, + .windowRatio = windowRatio, + .windowShare = windowShare, + .method = placementMethod, + .deepeningFactor = deepeningFactor, + .deepeningValue = deepeningValue, + .lookaheadFactor = lookaheadFactor, + .reuseLevel = reuseLevel, + .maxNodes = maxNodes, + .trials = trials, + .queueCapacity = queueCapacity, + }; + config.layoutSynthesizerConfig.routerConfig = { + .method = routingMethod, .preferSplit = preferSplit}; + config.codeGeneratorConfig = {.warnUnsupportedGates = + warnUnsupportedGates}; + return {arch, config}; + }), py::keep_alive<1, 2>(), "arch"_a, "log_level"_a = spdlog::level::to_short_c_str(defaultConfig.logLevel), "max_filling_factor"_a = defaultConfig.schedulerConfig.maxFillingFactor, @@ -182,6 +199,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { defaultConfig.layoutSynthesizerConfig.placerConfig.windowRatio, "window_share"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.windowShare, + "placement_method"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.method, "deepening_factor"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.deepeningFactor, "deepening_value"_a = @@ -192,6 +211,9 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { defaultConfig.layoutSynthesizerConfig.placerConfig.reuseLevel, "max_nodes"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.maxNodes, + "trials"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.trials, + "queue_capacity"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.queueCapacity, "routing_method"_a = defaultConfig.layoutSynthesizerConfig.routerConfig.method, "prefer_split"_a = diff --git a/include/na/zoned/Compiler.hpp b/include/na/zoned/Compiler.hpp index 6546d22b8..1c513b7d2 100644 --- a/include/na/zoned/Compiler.hpp +++ b/include/na/zoned/Compiler.hpp @@ -15,7 +15,7 @@ #include "ir/QuantumComputation.hpp" #include "ir/operations/Operation.hpp" #include "layout_synthesizer/PlaceAndRouteSynthesizer.hpp" -#include "layout_synthesizer/placer/AStarPlacer.hpp" +#include "layout_synthesizer/placer/HeuristicPlacer.hpp" #include "layout_synthesizer/placer/VertexMatchingPlacer.hpp" #include "layout_synthesizer/router/IndependentSetRouter.hpp" #include "na/NAComputation.hpp" @@ -269,7 +269,7 @@ class RoutingAgnosticCompiler final }; class RoutingAwareSynthesizer - : public PlaceAndRouteSynthesizer { public: RoutingAwareSynthesizer(const Architecture& architecture, diff --git a/include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp similarity index 60% rename from include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp rename to include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 7236d0396..f5a5c9422 100644 --- a/include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -41,11 +43,11 @@ using RowColumnMap = using RowColumnSet = std::unordered_set, size_t>>; /** - * @brief The A* placer is a class that provides a method to determine the - * placement of the atoms in each layer using the A* search algorithm. + * @brief The heuristic placer is a class that provides a method to determine + * the placement of the atoms in each layer using a heuristic search algorithm. */ -class AStarPlacer : public PlacerBase { - friend class AStarPlacerTest_AStarSearch_Test; +class HeuristicPlacer : public PlacerBase { + friend class HeuristicPlacerTest_AStarSearch_Test; using DiscreteSite = std::array; using CompatibilityGroup = std::array, 2>; @@ -68,7 +70,7 @@ class AStarPlacer : public PlacerBase { size_t windowMinHeight_; public: - /// The configuration of the A* placer + /// The configuration of the heuristic placer struct Config { /** * @brief This flag indicates whether the placement should use a window when @@ -81,7 +83,7 @@ class AStarPlacer : public PlacerBase { * rows. The window is centered at the nearest site. * @note Specified by the user in the configuration file. */ - size_t windowMinWidth = 8; + size_t windowMinWidth = 16; /** * @brief If the window is used, this denotes the ratio between the height * and the width of the window. @@ -104,7 +106,16 @@ class AStarPlacer : public PlacerBase { * moved will end in the same window. * @note Specified by the user in the configuration file. */ - double windowShare = 0.6; + double windowShare = 0.8; + /// Enum of available heuristic methods used for the search. + enum class Method : uint8_t { + /// A-star algorithm + ASTAR, + /// Iterative diving search + IDS + }; + /// The heuristic method used for the search (default: IDS). + Method method = Method::IDS; /** * @brief The heuristic used in the A* search contains a term that resembles * the standard deviation of the differences between the current and target @@ -115,14 +126,14 @@ class AStarPlacer : public PlacerBase { * heuristic. However, this leads to a vast exploration of the search tree * and usually results in a huge number of nodes visited. */ - float deepeningFactor = 0.8F; + float deepeningFactor = 0.01F; /** * @brief Before the sum of standard deviations is multiplied with the * number of unplaced nodes and @ref deepeningFactor_, this value is added * to the sum to amplify the influence of the unplaced nodes count. * @see deepeningFactor_ */ - float deepeningValue = 0.2F; + float deepeningValue = 0.0F; /** * @brief The cost function can consider the distance of atoms to their * interaction partner in the next layer. @@ -131,7 +142,7 @@ class AStarPlacer : public PlacerBase { * entirely. A factor of 1.0 implies that the lookahead is as important as * the distance to the target site, which is usually not desired. */ - float lookaheadFactor = 0.2F; + float lookaheadFactor = 0.4F; /** * @brief The reuse level corresponds to the estimated extra fidelity loss * due to the extra trap transfers when the atom is not reused and instead @@ -144,16 +155,38 @@ class AStarPlacer : public PlacerBase { * @brief The maximum number of nodes that are allowed to be visited in the * A* search tree. * @detsils 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. + * is raised. + */ + size_t maxNodes = 10'000'000; + /** + * @brief The number of trials determines the number of restarts during IDS. + * @note This option is only relevant if the IDS heuristic method is used. + */ + size_t trials = 4; + /** + * @brief The maximum capacity of the priority queue used during IDS. + * @note This option is only relevant if the IDS heuristic method is used. + */ + size_t queueCapacity = 100; + + private: + /// @returns the default configuration for the A* method + [[nodiscard]] static auto createWithAStarDefaults() -> Config; + + /// @returns the default configuration for the IDS method + [[nodiscard]] static auto createWithIDSDefaults() -> Config; + + public: + /** + * @param method the method to use for the placer + * @returns the default configuration for a given method */ - size_t maxNodes = 50'000'000; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, useWindow, - windowMinWidth, windowRatio, - windowShare, deepeningFactor, - deepeningValue, lookaheadFactor, - reuseLevel, maxNodes); + [[nodiscard]] static auto createForMethod(Method method) -> Config; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( + Config, useWindow, windowMinWidth, windowRatio, windowShare, method, + deepeningFactor, deepeningValue, lookaheadFactor, reuseLevel, maxNodes, + trials, queueCapacity); }; private: @@ -223,11 +256,13 @@ class AStarPlacer : public PlacerBase { * stage */ struct AtomNode { + /// The parent node. + std::shared_ptr parent = nullptr; /** * The current level in the search tree. A level equal to the number of * atoms to be placed indicates that all atoms have been placed. */ - uint8_t level = 0; + uint16_t level = 0; /** * The index of the chosen option for the current atom instead of a pointer * to that option to save memory @@ -259,11 +294,13 @@ class AStarPlacer : public PlacerBase { * stage. */ struct GateNode { + /// The parent node. + std::shared_ptr parent = nullptr; /** * The current level in the search tree. A level equal to the number of * gates to be placed indicates that all gates have been placed. */ - uint8_t level = 0; + uint16_t level = 0; /** * The index of the chosen option for the current gate instead of a pointer * to that option to save memory @@ -290,7 +327,7 @@ class AStarPlacer : public PlacerBase { public: /// Constructs an A* placer for the given architecture and configuration. - AStarPlacer(const Architecture& architecture, const Config& config); + HeuristicPlacer(const Architecture& architecture, const Config& config); /** * This function defines the interface of the placer and delegates the @@ -306,6 +343,357 @@ class AStarPlacer : public PlacerBase { -> std::vector override; private: + /** + * @brief This class implements a bounded priority queue. + * @details It uses two heaps to allow for efficient retrieval and removal of + * both the minimum and maximum elements. If the maximum capacity is reached, + * either the new element or the maximum element is discarded on insertion + * depending on which has a higher priority. + * @tparam T is the type of elements stored in the queue. + * @tparam Compare is the comparison functor to determine the priority of the + * elements. + */ + template > class BoundedPriorityQueue { + public: + using ValueType = T; + using SizeType = size_t; + using PriorityCompare = Compare; + using Reference = ValueType&; + + private: + struct Node { + SizeType minHeapIndex; + SizeType maxHeapIndex; + ValueType value; + Node(const SizeType minHeapIndex, const SizeType maxHeapIndex, + ValueType&& value) + : minHeapIndex(minHeapIndex), maxHeapIndex(maxHeapIndex), + value(std::move(value)) {} + }; + /// Vector of heap elements satisfying the min-heap property. + std::vector> minHeap_; + /// Vector of heap elements satisfying the max-heap property. + std::vector maxHeap_; + /// The maximum number of elements in the heap. + SizeType heapCapacity_; + /// The comparison functor used to determine priority. + PriorityCompare compare_; + + /** + * Starts to establish the min-heap property from the given index upwards. + * @param i is the index in the min-heap. + */ + auto heapifyMinHeapUp(SizeType i) -> void { + assert(i < minHeap_.size()); + while (i > 0) { + size_t parent = (i - 1) / 2; + if (compare_(minHeap_[i]->value, minHeap_[parent]->value)) { + std::swap(minHeap_[i], minHeap_[parent]); + minHeap_[i]->minHeapIndex = i; + minHeap_[parent]->minHeapIndex = parent; + i = parent; + } else { + break; + } + } + } + + /** + * Starts to establish the max-heap property from the given index upwards. + * @param i is the index in the max-heap. + */ + auto heapifyMaxHeapUp(SizeType i) -> void { + assert(i < maxHeap_.size()); + while (i > 0) { + size_t parent = (i - 1) / 2; + if (compare_(maxHeap_[parent]->value, maxHeap_[i]->value)) { + std::swap(maxHeap_[i], maxHeap_[parent]); + maxHeap_[i]->maxHeapIndex = i; + maxHeap_[parent]->maxHeapIndex = parent; + i = parent; + } else { + break; + } + } + } + + /** + * Starts to establish the min-heap property from the given index downwards. + * @param i is the index in the min-heap. + */ + auto heapifyMinHeapDown(SizeType i) -> void { + while (true) { + size_t leftChild = (2 * i) + 1; + size_t rightChild = (2 * i) + 2; + size_t smallest = i; + + if (leftChild < minHeap_.size() && + compare_(minHeap_[leftChild]->value, minHeap_[smallest]->value)) { + smallest = leftChild; + } + if (rightChild < minHeap_.size() && + compare_(minHeap_[rightChild]->value, minHeap_[smallest]->value)) { + smallest = rightChild; + } + if (smallest != i) { + std::swap(minHeap_[i], minHeap_[smallest]); + minHeap_[i]->minHeapIndex = i; + minHeap_[smallest]->minHeapIndex = smallest; + i = smallest; + } else { + break; + } + } + } + + /** + * Starts to establish the max-heap property from the given index downwards. + * @param i is the index in the max-heap. + */ + auto heapifyMaxHeapDown(SizeType i) -> void { + while (true) { + size_t leftChild = (2 * i) + 1; + size_t rightChild = (2 * i) + 2; + size_t largest = i; + + if (leftChild < maxHeap_.size() && + compare_(maxHeap_[largest]->value, maxHeap_[leftChild]->value)) { + largest = leftChild; + } + if (rightChild < maxHeap_.size() && + compare_(maxHeap_[largest]->value, maxHeap_[rightChild]->value)) { + largest = rightChild; + } + if (largest != i) { + std::swap(maxHeap_[i], maxHeap_[largest]); + maxHeap_[i]->maxHeapIndex = i; + maxHeap_[largest]->maxHeapIndex = largest; + i = largest; + } else { + break; + } + } + } + + public: + /** + * Constructs a new instance of the bounded priority queue. + * @param maxQueueSize is the maximum number of elements that can be stored + * in the queue. + * @note A capacity of 0 results in a no-op queue where push() discards all + * elements + */ + explicit BoundedPriorityQueue(const SizeType maxQueueSize) + : heapCapacity_(maxQueueSize) { + minHeap_.reserve(heapCapacity_); + maxHeap_.reserve(heapCapacity_); + } + /** + * @returns the top element of the priority queue. + * @note If @ref empty returns `true`, calling this function is + * undefined behavior. + */ + [[nodiscard]] auto top() const -> const ValueType& { + assert(!minHeap_.empty()); + return minHeap_.front()->value; + } + /** + * @returns the top element of the priority queue. + * @note If @ref empty returns `true`, calling this function is + * undefined behavior. + */ + [[nodiscard]] auto top() -> ValueType& { + assert(!minHeap_.empty()); + return minHeap_.front()->value; + } + /** + * @brief Removes the top element. + * @note If @ref empty returns `true`, calling this function is + * undefined behavior. + */ + auto pop() -> void { + assert(!minHeap_.empty()); + assert(minHeap_.size() == maxHeap_.size()); + assert(minHeap_.size() <= heapCapacity_); + if (minHeap_.size() == 1) { + minHeap_.pop_back(); + maxHeap_.pop_back(); + } else { + assert(minHeap_.size() > 1); + const auto i = minHeap_.front()->maxHeapIndex; + std::swap(minHeap_.front(), minHeap_.back()); + minHeap_.pop_back(); + minHeap_.front()->minHeapIndex = 0; + heapifyMinHeapDown(0); + if (i == maxHeap_.size() - 1) { + maxHeap_.pop_back(); + } else { + std::swap(maxHeap_[i], maxHeap_.back()); + maxHeap_.pop_back(); + maxHeap_[i]->maxHeapIndex = i; + // Restoring heap property may require moving the swapped element up + // or down. + if (i > 0) { + const size_t parent = (i - 1) / 2; + if (compare_(maxHeap_[parent]->value, maxHeap_[i]->value)) { + heapifyMaxHeapUp(i); + } else { + heapifyMaxHeapDown(i); + } + } else { + heapifyMaxHeapDown(i); + } + } + } + assert(minHeap_.size() == maxHeap_.size()); + assert(minHeap_.size() <= heapCapacity_); + } + /// @returns `true` if the queue is empty. + [[nodiscard]] auto empty() const -> bool { return minHeap_.empty(); } + /// @brief Inserts an element into the priority queue. + auto push(ValueType&& value) -> void { + assert(minHeap_.size() == maxHeap_.size()); + if (heapCapacity_ > 0) { + if (minHeap_.size() < heapCapacity_) { + minHeap_.emplace_back(std::make_unique( + minHeap_.size(), maxHeap_.size(), std::move(value))); + maxHeap_.emplace_back(minHeap_.back().get()); + heapifyMinHeapUp(minHeap_.size() - 1); + heapifyMaxHeapUp(maxHeap_.size() - 1); + } else { + assert(minHeap_.size() == heapCapacity_); + // if capacity is reached, only insert the value if smaller than max + if (compare_(value, maxHeap_.front()->value)) { + const auto i = maxHeap_.front()->minHeapIndex; + assert(i < minHeap_.size()); + minHeap_[i] = std::make_unique(i, 0, std::move(value)); + maxHeap_.front() = minHeap_[i].get(); + heapifyMinHeapUp(i); + heapifyMaxHeapDown(0); + } + } + } + assert(minHeap_.size() == maxHeap_.size()); + assert(minHeap_.size() <= heapCapacity_); + } + /** + * @brief Removes the top element and decrements the maximum capacity by + * one. + * @note If @ref empty returns `true`, calling this function is + * undefined behavior. + */ + auto popAndShrink() -> void { + pop(); + if (heapCapacity_ > 0) { + --heapCapacity_; + } + assert(minHeap_.size() == maxHeap_.size()); + assert(minHeap_.size() <= heapCapacity_); + } + }; + + /** + * @brief Performs an iterative diving search (IDS) on the defined graph. + * @tparam Node is the type of nodes in the graph. + * @param start is the start node to start the search from. + * @param getNeighbors is a function to retrieve the neighbor reachable from a + * given node. + * @param isGoal is a function to determine whether a given node is a goal + * node. + * @param getCost is a function returning the cost of a node in the graph. The + * cost of the start node must be 0. + * @param getHeuristic is a function returning the heuristic value of a given + * node. + * @param trials is the number of attempts to find a goal node that are + * performed at most. This parameter must be greater than 0. + * @param queueCapacity is the capacity of the queue used for the iterative + * diving search. For the actual capacity, the current value of trial is + * added. + * @return a shared pointer to the final node of the search. + */ + template + [[nodiscard]] static auto iterativeDivingSearch( + std::shared_ptr start, + const std::function>( + std::shared_ptr)>& getNeighbors, + const std::function& isGoal, + const std::function& getCost, + const std::function& getHeuristic, size_t trials, + const size_t queueCapacity) -> std::shared_ptr { + if (trials == 0) { + throw std::invalid_argument("IDS requires trials >= 1"); + } + struct Item { + double priority; //< sum of cost and heuristic + std::shared_ptr node; //< pointer to the node + + Item(const double priority, std::shared_ptr node) + : priority(priority), node(node) { + assert(!std::isnan(priority)); + } + }; + struct ItemCompare { + auto operator()(const Item& a, const Item& b) const -> bool { + return a.priority < b.priority; + } + }; + // Initial capacity accounts for shrinking: popAndShrink() decrements + // capacity on each trial + BoundedPriorityQueue queue(queueCapacity + trials); + std::optional goal; + Item currentItem{getHeuristic(*start), start}; + while (true) { + if (isGoal(*currentItem.node)) { + SPDLOG_TRACE("Goal node found with priority {}", currentItem.priority); + trials--; + if (!goal.has_value() || currentItem.priority < goal->priority) { + goal = std::move(currentItem); + } + if (trials > 0 && !queue.empty()) { + SPDLOG_TRACE("Restart search with priority {}", goal->priority); + currentItem = std::move(queue.top()); + queue.popAndShrink(); + continue; + } + break; + } + // Expand the current node by adding all neighbors to the open set + std::optional minItem = std::nullopt; + for (auto& neighbor : getNeighbors(currentItem.node)) { + const auto cost = getCost(*neighbor); + const auto heuristic = getHeuristic(*neighbor); + Item item{cost + heuristic, std::move(neighbor)}; + if (!minItem) { + minItem = std::move(item); + } else if (item.priority < minItem->priority) { + queue.push(std::move(*minItem)); + minItem = std::move(item); + } else { + queue.push(std::move(item)); + } + } + if (minItem) { + currentItem = std::move(*minItem); + } else { + assert(trials > 0); + if (!queue.empty()) { + SPDLOG_TRACE("No neighbors found, restarting search"); + currentItem = std::move(queue.top()); + queue.pop(); + } else { + break; + } + } + } + if (!goal) { + throw std::runtime_error( + "No path from start to any goal found. This may be caused by a too " + "narrow window size. Try adjusting the window_share compiler " + "configuration option to a higher value, such as 1.0."); + } + return goal->node; + } + /** * @brief A* search algorithm for trees * @details A* is a graph traversal and path search algorithm that finds the @@ -322,15 +710,12 @@ class AStarPlacer : public PlacerBase { * is superfluous for trees. * - As a consequence of the first point, this implementation also does not * check whether a node is already in the open set. This would also require an - * O(log(n)) check operation which is not necessary for trees as one path can + * O(log(n)) check operation, which is not necessary for trees as one path can * only reach a node. * @note This implementation of A* search can only handle trees and not * general graphs. This is because it does not keep track of visited nodes and - * therefore cannot detect cycles. Also for DAGs it may expand nodes multiple + * therefore cannot detect cycles. Also, for DAGs it may expand nodes multiple * times when they can be reached by different paths from the start node. - * @note @p getHeuristic must be admissible, meaning that it never - * overestimates the cost to reach the goal from the current node calculated - * by @p getCost for every edge on the path. * @note The calling program has to make sure that the pointers passed to this * function are valid and that the iterators are not invalidated during the * search, e.g., by calling one of the passed functions like @p getNeighbors. @@ -340,21 +725,83 @@ class AStarPlacer : public PlacerBase { * @param isGoal is a function that returns true if a node is one of * potentially multiple goals * @param getCost is a function that returns the total cost to reach that - * particular node from the start node + * particular node from the start node. The cost of the start node must be 0. * @param getHeuristic is a function that returns the heuristic cost from the * node to any goal. + * @param maxNodes is the maximum number of nodes held in the priority queue + * before the search is aborted. This parameter must be greater than 0. * @return a vector of node references representing the path from the start to * a goal */ template - [[nodiscard]] static auto aStarTreeSearch( - const Node& start, - const std::function>( - const Node&)>& getNeighbors, - const std::function& isGoal, - const std::function& getCost, - const std::function& getHeuristic, size_t maxNodes) - -> std::vector>; + [[nodiscard]] static auto + aStarTreeSearch(std::shared_ptr start, + const std::function>( + std::shared_ptr)>& getNeighbors, + const std::function& isGoal, + const std::function& getCost, + const std::function& getHeuristic, + size_t maxNodes) -> std::shared_ptr { + if (maxNodes == 0) { + throw std::invalid_argument("`maxNodes` must be greater than 0"); + } + //===--------------------------------------------------------------------===// + // Setup open set structure + //===--------------------------------------------------------------------===// + // struct for items in the open set + struct Item { + double priority; //< sum of cost and heuristic + std::shared_ptr node; //< pointer to the node + + Item(const double priority, std::shared_ptr node) + : priority(priority), node(node) { + assert(!std::isnan(priority)); + } + }; + // compare function for the open set + struct ItemCompare { + auto operator()(const Item& a, const Item& b) const -> bool { + // this way, the item with the lowest priority is on top of the heap + return a.priority > b.priority; + } + }; + // open list of nodes to be evaluated as a minimum heap based on the + // priority. + std::priority_queue, ItemCompare> openSet; + openSet.emplace(getHeuristic(*start), start); + //===--------------------------------------------------------------------===// + // Perform A* search + //===--------------------------------------------------------------------===// + while (!openSet.empty()) { + auto itm = openSet.top(); + openSet.pop(); + // if a goal is reached, that is the shortest path to a goal under the + // assumption that the heuristic is admissible + if (isGoal(*itm.node)) { + return itm.node; + } + // expand the current node by adding all neighbors to the open set + const auto& neighbors = getNeighbors(itm.node); + if (!neighbors.empty()) { + for (const auto& neighbor : neighbors) { + // getCost returns the total cost to reach the current node + const auto cost = getCost(*neighbor); + const auto heuristic = getHeuristic(*neighbor); + openSet.emplace(cost + heuristic, neighbor); + } + } + if (openSet.size() >= maxNodes) { + throw std::runtime_error( + "Maximum number of nodes reached. Increase max_nodes or increase " + "deepening_value and deepening_factor to reduce the number of " + "explored nodes."); + } + } + throw std::runtime_error( + "No path from start to any goal found. This may be caused by a too " + "narrow window size. Try adjusting the window_share compiler " + "configuration option to a higher value, such as 1.0."); + } /** * @brief This function takes a list of atoms together with their current @@ -363,7 +810,7 @@ class AStarPlacer : public PlacerBase { * @param placement is a list of atoms together with their current placement * @param atoms is a list of all atoms that must be placed * @return a pair of two maps, the first one maps rows to their discrete - * indices and the second one maps columns to their discrete indices + * indices, and the second one maps columns to their discrete indices */ [[nodiscard]] auto discretizePlacementOfAtoms(const Placement& placement, @@ -583,16 +1030,14 @@ class AStarPlacer : public PlacerBase { * whether the new corresponding placement is compatible with any of the * existing groups. If yes, the new placement is added to the respective group * and otherwise, a new group is formed with the new placement. - * @param nodes is the list of all nodes created so far with permanent memory - * allocation - * @param atomJobs are the atoms to be placed - * @param node is the node to be expanded + * @param atomJobs are the atoms to be placed. + * @param node is the node to be expanded. * @return a list of references to the neighbors of the given node */ [[nodiscard]] static auto - getNeighbors(std::deque>& nodes, - const std::vector& atomJobs, const AtomNode& node) - -> std::vector>; + getNeighbors(const std::vector& atomJobs, + const std::shared_ptr& node) + -> std::vector>; /** * @brief Return references to all neighbors of the given node. @@ -607,23 +1052,21 @@ class AStarPlacer : public PlacerBase { * whether the new corresponding placement is compatible with any of the * existing groups. If yes, the new placement is added to the respective group * and otherwise, a new group is formed with the new placement. - * @param nodes is the list of all nodes created so far with permanent memory - * allocation - * @param gateJobs are the gates to be placed - * @param node is the node to be expanded + * @param gateJobs are the gates to be placed. + * @param node is the node to be expanded. * @return a list of references to the neighbors of the given node */ [[nodiscard]] static auto - getNeighbors(std::deque>& nodes, - const std::vector& gateJobs, const GateNode& node) - -> std::vector>; + getNeighbors(const std::vector& gateJobs, + const std::shared_ptr& node) + -> std::vector>; /** * Checks the compatibility with a new assignment, i.e., a key-value pair, * whether it is compatible with an existing group. The group can either be * a horizontal or vertical group. In case the new assignment is compatible * with the group, an iterator is returned pointing to the assignment in the - * group, if it already exists or to the element directly following the + * group if it already exists or to the element directly following the * new key. Additionally, a boolean is returned indicating whether the new * exists in the group. * @param key the key of the new assignment @@ -684,4 +1127,10 @@ class AStarPlacer : public PlacerBase { const SLM& nearestSLM, size_t r, size_t c, GateJob& job) const -> void; }; +NLOHMANN_JSON_SERIALIZE_ENUM(HeuristicPlacer::Config::Method, + { + {HeuristicPlacer::Config::Method::ASTAR, + "astar"}, + {HeuristicPlacer::Config::Method::IDS, "ids"}, + }) } // namespace na::zoned diff --git a/pyproject.toml b/pyproject.toml index 5d135700f..76191c8f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Typing :: Typed", ] -requires-python = ">=3.10" +requires-python = ">=3.10, != 3.14.1" dependencies = [ "mqt.core~=3.3.3", "qiskit[qasm3-import]>=1.0.0", diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 977fe0996..3655e82cf 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -54,6 +54,18 @@ class ZonedNeutralAtomArchitecture: the architecture as a .namachine string """ +class PlacementMethod(Enum): + """Enumeration of the available placement methods for the heuristic placer.""" + + astar = ... + """ + A-star algorithm + """ + ids = ... + """ + Iterative diving search + """ + class RoutingMethod(Enum): """Enumeration of the available routing methods for the independent set router.""" @@ -143,11 +155,14 @@ class RoutingAwareCompiler: window_min_width: int = ..., window_ratio: float = ..., window_share: float = ..., + placement_method: PlacementMethod = ..., deepening_factor: float = ..., deepening_value: float = ..., lookahead_factor: float = ..., reuse_level: float = ..., max_nodes: int = ..., + trials: int = ..., + queue_capacity: int = ..., routing_method: RoutingMethod = ..., prefer_split: float = ..., warn_unsupported_gates: bool = ..., @@ -166,6 +181,8 @@ class RoutingAwareCompiler: 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 @@ -183,6 +200,8 @@ class RoutingAwareCompiler: 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. diff --git a/src/na/zoned/layout_synthesizer/placer/AStarPlacer.cpp b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp similarity index 76% rename from src/na/zoned/layout_synthesizer/placer/AStarPlacer.cpp rename to src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp index 1dfa4f11f..135e193f4 100644 --- a/src/na/zoned/layout_synthesizer/placer/AStarPlacer.cpp +++ b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp @@ -8,7 +8,7 @@ * Licensed under the MIT License */ -#include "na/zoned/layout_synthesizer/placer/AStarPlacer.hpp" +#include "na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp" #include "ir/Definitions.hpp" #include "na/zoned/Architecture.hpp" @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -30,7 +29,7 @@ #include #include #include -#include +#include #include #include #include @@ -39,98 +38,15 @@ #include namespace na::zoned { -template -auto AStarPlacer::aStarTreeSearch( - const Node& start, - const std::function>( - const Node&)>& getNeighbors, - const std::function& isGoal, - const std::function& getCost, - const std::function& getHeuristic, - const size_t maxNodes) -> std::vector> { - //===--------------------------------------------------------------------===// - // Setup open set structure - //===--------------------------------------------------------------------===// - // struct for items in the open set - struct Item { - double priority_; //< sum of cost and heuristic - const Node* node_; //< pointer to the node - // pointer to the parent item to reconstruct the path in the end - Item* parent_; - - Item(const double priority, const Node& node, Item* parent) - : priority_(priority), node_(&node), parent_(parent) { - assert(!std::isnan(priority)); - } - }; - // compare function for the open set - struct ItemCompare { - auto operator()(const Item* a, const Item* b) const -> bool { - // this way, the item with the lowest priority is on top of the heap - return a->priority_ > b->priority_; - } - }; - // vector of items to store all items and keep them alive also after they - // are popped from the open set. they are required alive to reconstruct the - // path in the end. - std::vector> items; - // open list of nodes to be evaluated as a minimum heap based on the - // priority. whenever an item is placed in the queue it is created in the - // vector `items` before and only a reference is placed in the queue - std::priority_queue, ItemCompare> openSet; - openSet.emplace(items - .emplace_back(std::make_unique(getHeuristic(start), - start, nullptr)) - .get()); - //===--------------------------------------------------------------------===// - // Perform A* search - //===--------------------------------------------------------------------===// - while (items.size() < maxNodes && !openSet.empty()) { - Item* itm = openSet.top(); - openSet.pop(); - // if a goal is reached, that is the shortest path to a goal under the - // assumption that the heuristic is admissible - if (isGoal(*itm->node_)) { - // reconstruct the path from the goal to the start and then reverse it - std::vector> path; - for (; itm != nullptr; itm = itm->parent_) { - path.emplace_back(*itm->node_); - } - std::reverse(path.begin(), path.end()); - return path; - } - // expand the current node by adding all neighbors to the open set - const auto& neighbors = getNeighbors(*itm->node_); - if (!neighbors.empty()) { - for (const auto& neighbor : neighbors) { - // getCost returns the total cost to reach the current node - const auto cost = getCost(neighbor); - const auto heuristic = getHeuristic(neighbor); - openSet.emplace(items - .emplace_back(std::make_unique( - cost + heuristic, neighbor, itm)) - .get()); - } - } - } - if (items.size() >= maxNodes) { - throw std::runtime_error( - "Maximum number of nodes reached. Increase max_nodes or increase " - "deepening_value and deepening_factor to reduce the number of explored " - "nodes."); - } - throw std::runtime_error( - "No path from start to any goal found. This may be caused by a too " - "narrow window size. Try adjusting the window_share compiler " - "configuration option to a higher value, such as 1.0."); -} -auto AStarPlacer::isGoal(const size_t nGates, const GateNode& node) -> bool { +auto HeuristicPlacer::isGoal(const size_t nGates, const GateNode& node) + -> bool { return node.level == nGates; } -auto AStarPlacer::isGoal(const size_t nAtoms, const AtomNode& node) -> bool { +auto HeuristicPlacer::isGoal(const size_t nAtoms, const AtomNode& node) + -> bool { return node.level == nAtoms; } -auto AStarPlacer::discretizePlacementOfAtoms( +auto HeuristicPlacer::discretizePlacementOfAtoms( const Placement& placement, const std::vector& atoms) const -> std::pair, RowColumnMap> { std::map rows; @@ -138,12 +54,12 @@ auto AStarPlacer::discretizePlacementOfAtoms( for (const auto atom : atoms) { const auto& [slm, r, c] = placement[atom]; const auto& [x, y] = architecture_.get().exactSLMLocation(slm, r, c); - rows.try_emplace(y).first->second.emplace(std::pair{std::cref(slm), r}); - columns.try_emplace(x).first->second.emplace(std::pair{std::cref(slm), c}); + rows.try_emplace(y).first->second.emplace(std::cref(slm), r); + columns.try_emplace(x).first->second.emplace(std::cref(slm), c); } RowColumnMap rowIndices; uint8_t rowIndex = 0; - for (const auto& [_, sites] : rows) { + for (const auto& sites : rows | std::views::values) { for (const auto& site : sites) { rowIndices.emplace(site, rowIndex); } @@ -151,7 +67,7 @@ auto AStarPlacer::discretizePlacementOfAtoms( } RowColumnMap columnIndices; uint8_t columnIndex = 0; - for (const auto& [_, sites] : columns) { + for (const auto& sites : columns | std::views::values) { for (const auto& site : sites) { columnIndices.emplace(site, columnIndex); } @@ -160,7 +76,7 @@ auto AStarPlacer::discretizePlacementOfAtoms( return std::pair{rowIndices, columnIndices}; } -auto AStarPlacer::discretizeNonOccupiedStorageSites( +auto HeuristicPlacer::discretizeNonOccupiedStorageSites( const SiteSet& occupiedSites) const -> std::pair, RowColumnMap> { std::map, size_t>> rows; @@ -170,7 +86,7 @@ auto AStarPlacer::discretizeNonOccupiedStorageSites( // find rows with free sites for (size_t r = 0; r < slm->nRows; ++r) { for (size_t c = 0; c < slm->nCols; ++c) { - if (occupiedSites.find(std::tie(*slm, r, c)) == occupiedSites.end()) { + if (!occupiedSites.contains(std::tie(*slm, r, c))) { // free site in row r found at column c rows.emplace(slm->location.second + (slm->siteSeparation.second * r), std::pair{std::cref(*slm), r}); @@ -181,7 +97,7 @@ auto AStarPlacer::discretizeNonOccupiedStorageSites( // find columns with free sites for (size_t c = 0; c < slm->nCols; ++c) { for (size_t r = 0; r < slm->nRows; ++r) { - if (occupiedSites.find(std::tie(*slm, r, c)) == occupiedSites.end()) { + if (!occupiedSites.contains(std::tie(*slm, r, c))) { // free site in column c found at row r columns.emplace(slm->location.first + (slm->siteSeparation.first * c), std::pair{std::cref(*slm), c}); @@ -192,18 +108,18 @@ auto AStarPlacer::discretizeNonOccupiedStorageSites( } RowColumnMap rowIndices; uint8_t rowIndex = 0; - for (const auto& [_, site] : rows) { + for (const auto& site : rows | std::views::values) { rowIndices.emplace(site, rowIndex++); } RowColumnMap columnIndices; uint8_t columnIndex = 0; - for (const auto& [_, site] : columns) { + for (const auto& site : columns | std::views::values) { columnIndices.emplace(site, columnIndex++); } return std::pair{rowIndices, columnIndices}; } -auto AStarPlacer::discretizeNonOccupiedEntanglementSites( +auto HeuristicPlacer::discretizeNonOccupiedEntanglementSites( const SiteSet& occupiedSites) const -> std::pair, RowColumnMap> { std::map rows; @@ -213,11 +129,11 @@ auto AStarPlacer::discretizeNonOccupiedEntanglementSites( // find rows with free sites for (size_t r = 0; r < slm.nRows; ++r) { for (size_t c = 0; c < slm.nCols; ++c) { - if (occupiedSites.find(std::tie(slm, r, c)) == occupiedSites.end()) { + if (!occupiedSites.contains(std::tie(slm, r, c))) { // free site in row r found at column c rows.try_emplace(slm.location.second + (slm.siteSeparation.second * r)) - .first->second.emplace(std::pair{std::cref(slm), r}); + .first->second.emplace(std::cref(slm), r); break; } } @@ -225,12 +141,12 @@ auto AStarPlacer::discretizeNonOccupiedEntanglementSites( // find columns with free sites for (size_t c = 0; c < slm.nCols; ++c) { for (size_t r = 0; r < slm.nRows; ++r) { - if (occupiedSites.find(std::tie(slm, r, c)) == occupiedSites.end()) { + if (!occupiedSites.contains(std::tie(slm, r, c))) { // free site in column c found at row r columns .try_emplace(slm.location.first + (slm.siteSeparation.first * c)) - .first->second.emplace(std::pair{std::cref(slm), c}); + .first->second.emplace(std::cref(slm), c); break; } } @@ -239,7 +155,7 @@ auto AStarPlacer::discretizeNonOccupiedEntanglementSites( } RowColumnMap rowIndices; uint8_t rowIndex = 0; - for (const auto& [_, sites] : rows) { + for (const auto& sites : rows | std::views::values) { for (const auto& site : sites) { rowIndices.emplace(site, rowIndex); } @@ -247,7 +163,7 @@ auto AStarPlacer::discretizeNonOccupiedEntanglementSites( } RowColumnMap columnIndices; uint8_t columnIndex = 0; - for (const auto& [_, sites] : columns) { + for (const auto& sites : columns | std::views::values) { for (const auto& site : sites) { columnIndices.emplace(site, columnIndex); } @@ -256,7 +172,7 @@ auto AStarPlacer::discretizeNonOccupiedEntanglementSites( return std::pair{rowIndices, columnIndices}; } -auto AStarPlacer::makeInitialPlacement(const size_t nQubits) const +auto HeuristicPlacer::makeInitialPlacement(const size_t nQubits) const -> Placement { auto slmIt = architecture_.get().storageZones.cbegin(); std::size_t c = 0; @@ -283,7 +199,7 @@ auto AStarPlacer::makeInitialPlacement(const size_t nQubits) const return initialPlacement; } -auto AStarPlacer::makeIntermediatePlacement( +auto HeuristicPlacer::makeIntermediatePlacement( const Placement& previousPlacement, const std::unordered_set& previousReuseQubits, const std::unordered_set& reuseQubits, @@ -293,12 +209,13 @@ auto AStarPlacer::makeIntermediatePlacement( const auto& gatePlacement = placeGatesInEntanglementZone( previousPlacement, previousReuseQubits, twoQubitGates, reuseQubits, nextTwoQubitGates); + SPDLOG_TRACE("Placed gates"); return {gatePlacement, placeAtomsInStorageZone(gatePlacement, reuseQubits, twoQubitGates, nextTwoQubitGates)}; } -auto AStarPlacer::addGateOption( +auto HeuristicPlacer::addGateOption( const RowColumnMap& discreteTargetRows, const RowColumnMap& discreteTargetColumns, const SLM& leftSLM, const size_t leftRow, const size_t leftCol, const SLM& rightSLM, @@ -351,7 +268,7 @@ auto AStarPlacer::addGateOption( } } -auto AStarPlacer::placeGatesInEntanglementZone( +auto HeuristicPlacer::placeGatesInEntanglementZone( const Placement& previousPlacement, const std::unordered_set& reuseQubits, const TwoQubitGateLayer& twoQubitGates, @@ -367,10 +284,10 @@ auto AStarPlacer::placeGatesInEntanglementZone( for (const auto& gate : twoQubitGates) { const auto& [first, second] = gate; if (const auto firstQubitReuse = - reuseQubits.find(first) != reuseQubits.end() && + reuseQubits.contains(first) && std::get<0>(previousPlacement[first]).get().isEntanglement(); !firstQubitReuse && - (reuseQubits.find(second) == reuseQubits.end() || + (!reuseQubits.contains(second) || std::get<0>(previousPlacement[second]).get().isStorage())) { const auto& [storageSLM1, storageRow1, storageCol1] = previousPlacement[first]; @@ -425,7 +342,7 @@ auto AStarPlacer::placeGatesInEntanglementZone( currentPlacement[second] = architecture_.get().otherEntanglementSite(slm, r, c); } else { - // second qubit is reused + // the second qubit is reused const auto& [slm, r, c] = previousPlacement[second]; currentPlacement[first] = architecture_.get().otherEntanglementSite(slm, r, c); @@ -484,7 +401,7 @@ auto AStarPlacer::placeGatesInEntanglementZone( */ std::vector gateJobs; gateJobs.reserve(nJobs); - for (const auto& [_, gate] : gatesToPlace) { + for (const auto& gate : gatesToPlace | std::views::values) { const auto& [leftAtom, rightAtom] = gate; const auto& [leftSLM, leftRow, leftCol] = previousPlacement[leftAtom]; const auto& [rightSLM, rightRow, rightCol] = previousPlacement[rightAtom]; @@ -517,8 +434,7 @@ auto AStarPlacer::placeGatesInEntanglementZone( } for (size_t r = rLow; r < rHigh; ++r) { for (size_t c = cLow; c < cHigh; ++c) { - if (occupiedEntanglementSites.find(std::tie(nearestSLM, r, c)) == - occupiedEntanglementSites.end()) { + if (!occupiedEntanglementSites.contains(std::tie(nearestSLM, r, c))) { addGateOption(discreteTargetRows, discreteTargetColumns, leftSLM, leftRow, leftCol, rightSLM, rightRow, rightCol, nearestSLM, r, c, job); @@ -529,17 +445,17 @@ auto AStarPlacer::placeGatesInEntanglementZone( while (config_.useWindow && static_cast(job.options.size()) < config_.windowShare * static_cast(nJobs)) { - // window does not contain enough options, so expand it + // the window does not contain enough options, so expand it ++expansion; size_t windowWidth = 0; size_t windowHeight = 0; if (config_.windowRatio < 1.0) { - // landscape ==> expand width and adjust height + // landscapes ==> expand width and adjust height windowWidth = config_.windowMinWidth + expansion; windowHeight = static_cast( std::round(config_.windowRatio * static_cast(windowWidth))); } else { - // portrait ==> expand height and adjust width + // portraits ==> expand height and adjust width windowHeight = windowMinHeight_ + expansion; windowWidth = static_cast(std::round( static_cast(windowHeight) / config_.windowRatio)); @@ -555,8 +471,8 @@ auto AStarPlacer::placeGatesInEntanglementZone( if (rLowNew < rLow) { assert(rLow - rLowNew == 1); for (size_t c = cLowNew; c < cHighNew; ++c) { - if (occupiedEntanglementSites.find(std::tie( - nearestSLM, rLowNew, c)) == occupiedEntanglementSites.end()) { + if (!occupiedEntanglementSites.contains( + std::tie(nearestSLM, rLowNew, c))) { addGateOption(discreteTargetRows, discreteTargetColumns, leftSLM, leftRow, leftCol, rightSLM, rightRow, rightCol, nearestSLM, rLowNew, c, job); @@ -567,8 +483,8 @@ auto AStarPlacer::placeGatesInEntanglementZone( assert(rHighNew - rHigh == 1); for (size_t c = cLowNew; c < cHighNew; ++c) { // NOTE: we have to use rHighNew - 1 here, which is equal to rHigh - if (occupiedEntanglementSites.find(std::tie(nearestSLM, rHigh, c)) == - occupiedEntanglementSites.end()) { + if (!occupiedEntanglementSites.contains( + std::tie(nearestSLM, rHigh, c))) { addGateOption(discreteTargetRows, discreteTargetColumns, leftSLM, leftRow, leftCol, rightSLM, rightRow, rightCol, nearestSLM, rHigh, c, job); @@ -578,8 +494,8 @@ auto AStarPlacer::placeGatesInEntanglementZone( if (cLowNew < cLow) { assert(cLow - cLowNew == 1); for (size_t r = rLow; r < rHigh; ++r) { - if (occupiedEntanglementSites.find(std::tie( - nearestSLM, r, cLowNew)) == occupiedEntanglementSites.end()) { + if (!occupiedEntanglementSites.contains( + std::tie(nearestSLM, r, cLowNew))) { addGateOption(discreteTargetRows, discreteTargetColumns, leftSLM, leftRow, leftCol, rightSLM, rightRow, rightCol, nearestSLM, r, cLowNew, job); @@ -590,8 +506,8 @@ auto AStarPlacer::placeGatesInEntanglementZone( assert(cHighNew - cHigh == 1); for (size_t r = rLow; r < rHigh; ++r) { // NOTE: we have to use cHighNew - 1 here, which is equal to cHigh - if (occupiedEntanglementSites.find(std::tie(nearestSLM, r, cHigh)) == - occupiedEntanglementSites.end()) { + if (!occupiedEntanglementSites.contains( + std::tie(nearestSLM, r, cHigh))) { addGateOption(discreteTargetRows, discreteTargetColumns, leftSLM, leftRow, leftCol, rightSLM, rightRow, rightCol, nearestSLM, r, cHigh, job); @@ -603,19 +519,19 @@ auto AStarPlacer::placeGatesInEntanglementZone( cLow = cLowNew; cHigh = cHighNew; } - std::sort( - job.options.begin(), job.options.end(), + std::ranges::sort( + job.options, [](const GateJob::Option& lhs, const GateJob::Option& rhs) -> bool { return lhs.distance < rhs.distance; }); - // Determine whether lookahead for the gate should be considered. + // Determine whether a lookahead for the gate should be considered. // That is the case if the gate to be placed contains a reuse qubit // because then we do not only decide the position of the gate in this layer // but also of the gate in the next layer. - bool leftReuse = nextReuseQubits.find(leftAtom) != nextReuseQubits.end(); - bool rightReuse = nextReuseQubits.find(rightAtom) != nextReuseQubits.end(); - qc::Qubit nextInteractionPartner = 0; + bool leftReuse = nextReuseQubits.contains(leftAtom); + bool rightReuse = nextReuseQubits.contains(rightAtom); if (leftReuse || rightReuse) { + qc::Qubit nextInteractionPartner = 0; for (const auto& nextGate : nextTwoQubitGates) { const auto& [nextLeftAtom, nextRightAtom] = nextGate; if (leftReuse) { @@ -660,39 +576,34 @@ auto AStarPlacer::placeGatesInEntanglementZone( assert(!discreteRows.empty()); // ==> the following std::max_element does not // return a nullptr const uint8_t maxDiscreteSourceRow = - std::max_element(discreteRows.begin(), discreteRows.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }) - ->second; + std::ranges::max_element(discreteRows, [](const auto& lhs, + const auto& rhs) { + return lhs.second < rhs.second; + })->second; assert( !discreteColumns.empty()); // ==> the following std::max_element does not // return a nullptr const uint8_t maxDiscreteSourceColumn = - std::max_element(discreteColumns.begin(), discreteColumns.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }) - ->second; + std::ranges::max_element(discreteColumns, [](const auto& lhs, + const auto& rhs) { + return lhs.second < rhs.second; + })->second; assert(!discreteTargetRows .empty()); // ==> the following std::max_element does not // return a nullptr const uint8_t maxDiscreteTargetRow = - std::max_element(discreteTargetRows.begin(), discreteTargetRows.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }) - ->second; + std::ranges::max_element(discreteTargetRows, [](const auto& lhs, + const auto& rhs) { + return lhs.second < rhs.second; + })->second; assert(!discreteTargetColumns .empty()); // ==> the following std::max_element does not // return a nullptr const uint8_t maxDiscreteTargetColumn = - std::max_element(discreteTargetColumns.begin(), - discreteTargetColumns.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }) - ->second; + std::ranges::max_element(discreteTargetColumns, [](const auto& lhs, + const auto& rhs) { + return lhs.second < rhs.second; + })->second; // return a nullptr const std::array scaleFactors{ std::min(1.F, static_cast(1 + maxDiscreteTargetRow) / @@ -700,47 +611,60 @@ auto AStarPlacer::placeGatesInEntanglementZone( std::min(1.F, static_cast(1 + maxDiscreteTargetColumn) / static_cast(1 + maxDiscreteSourceColumn))}; //===------------------------------------------------------------------===// - // Run the A* algorithm + // Run the DFS algorithm //===------------------------------------------------------------------===// - /** - * A list of all nodes that have been created so far. - * This happens when a node is expanded by calling getNeighbors. - */ - std::deque> nodes; - // make the root node - nodes.emplace_back(std::make_unique()); const auto deepeningFactor = config_.deepeningFactor; const auto deepeningValue = config_.deepeningValue; - const auto& path = aStarTreeSearch( - *nodes.front(), - [&nodes, &gateJobs](const auto& node) { - return getNeighbors(nodes, gateJobs, std::move(node)); - }, - [nJobs](const auto& node) { return isGoal(nJobs, std::move(node)); }, - [](const auto& node) { return getCost(std::move(node)); }, - [&gateJobs, deepeningFactor, deepeningValue, - &scaleFactors](const auto& node) { - return getHeuristic(gateJobs, deepeningFactor, deepeningValue, - scaleFactors, std::move(node)); - }, - config_.maxNodes); + std::shared_ptr node; + switch (config_.method) { + case Config::Method::IDS: + node = iterativeDivingSearch( + std::make_shared(), + [&gateJobs](const auto& node) { return getNeighbors(gateJobs, node); }, + [nJobs](const auto& node) { return isGoal(nJobs, node); }, + [](const auto& node) { return getCost(node); }, + [&gateJobs, deepeningFactor, deepeningValue, + &scaleFactors](const auto& node) { + return getHeuristic(gateJobs, deepeningFactor, deepeningValue, + scaleFactors, node); + }, + config_.trials, config_.queueCapacity); + break; + case Config::Method::ASTAR: + node = aStarTreeSearch( + std::make_shared(), + [&gateJobs](const auto& node) { return getNeighbors(gateJobs, node); }, + [nJobs](const auto& node) { return isGoal(nJobs, node); }, + [](const auto& node) { return getCost(node); }, + [&gateJobs, deepeningFactor, deepeningValue, + &scaleFactors](const auto& node) { + return getHeuristic(gateJobs, deepeningFactor, deepeningValue, + scaleFactors, node); + }, + config_.maxNodes); + break; + default: + qc::unreachable(); + } //===------------------------------------------------------------------===// // Extract the final mapping //===------------------------------------------------------------------===// - assert(path.size() == nJobs + 1); - for (size_t i = 0; i < nJobs; ++i) { - const auto& job = gateJobs[i]; - const auto& option = job.options[path[i + 1].get().option]; + while (node->parent != nullptr) { + assert(0 < node->level); + assert(node->level <= gateJobs.size()); + const auto& job = gateJobs[node->level - 1]; + const auto& option = job.options[node->option]; for (size_t j = 0; j < 2; ++j) { const auto atom = job.qubits[j]; const auto& [row, col] = option.sites[j]; currentPlacement[atom] = targetSites.at(row).at(col); } + node = node->parent; } return currentPlacement; } -auto AStarPlacer::placeAtomsInStorageZone( +auto HeuristicPlacer::placeAtomsInStorageZone( const Placement& previousPlacement, const std::unordered_set& reuseQubits, const TwoQubitGateLayer& twoQubitGates, @@ -787,9 +711,9 @@ auto AStarPlacer::placeAtomsInStorageZone( architecture_.get().distance(slm, r, c, frontSLM, frontRow, frontCol); atomsWithoutFirstAtom.emplace(distance, *atomIt); } - std::transform(atomsWithoutFirstAtom.cbegin(), atomsWithoutFirstAtom.cend(), - atomsToPlace.begin() + 1, - [](const auto& pair) { return pair.second; }); + std::ranges::transform(std::as_const(atomsWithoutFirstAtom), + atomsToPlace.begin() + 1, + [](const auto& pair) { return pair.second; }); // Discretize the previous placement of the atoms to be placed that are // ordered now const auto& [discreteRows, discreteColumns] = @@ -869,7 +793,7 @@ auto AStarPlacer::placeAtomsInStorageZone( job.currentSite = std::array{ discreteRows.at(std::pair{std::cref(previousSLM), previousRow}), discreteColumns.at(std::pair{std::cref(previousSLM), previousCol})}; - if (reuseQubits.find(atom) != reuseQubits.end()) { + if (reuseQubits.contains(atom)) { // atom can be reused, so we add an option for the atom to stay at the // current site job.options.emplace_back(AtomJob::Option{{0, 0}, true, 0.0F}); @@ -892,8 +816,7 @@ auto AStarPlacer::placeAtomsInStorageZone( } for (size_t r = rLow; r < rHigh; ++r) { for (size_t c = cLow; c < cHigh; ++c) { - if (occupiedStorageSites.find(std::tie(nearestSLM, r, c)) == - occupiedStorageSites.end()) { + if (!occupiedStorageSites.contains(std::tie(nearestSLM, r, c))) { const auto distance = static_cast(architecture_.get().distance( previousSLM, previousRow, previousCol, nearestSLM, r, c)); job.options.emplace_back(AtomJob::Option{ @@ -908,19 +831,19 @@ auto AStarPlacer::placeAtomsInStorageZone( while (config_.useWindow && static_cast(job.options.size()) < config_.windowShare * static_cast(nJobs)) { - // window does not contain enough options, so expand it + // the window does not contain enough options, so expand it ++expansion; size_t windowWidth = 0; size_t windowHeight = 0; if (config_.windowRatio < 1.0) { - // landscpe ==> expand width and adjust height + // landscape ==> expand width and adjust height // the overall width and height is divided by 2 later, hence an // expansion of 2 is needed to actually increase the window size windowWidth = config_.windowMinWidth + 2 * expansion; windowHeight = static_cast( std::round(config_.windowRatio * static_cast(windowWidth))); } else { - // portrait ==> expand height and adjust width + // portraits ==> expand height and adjust width // the overall width and height is divided by 2 later, hence an // expansion of 2 is needed to actually increase the window size windowHeight = windowMinHeight_ + 2 * expansion; @@ -938,8 +861,8 @@ auto AStarPlacer::placeAtomsInStorageZone( if (rLowNew < rLow) { assert(rLow - rLowNew == 1); for (size_t c = cLowNew; c < cHighNew; ++c) { - if (occupiedStorageSites.find(std::tie(nearestSLM, rLowNew, c)) == - occupiedStorageSites.end()) { + if (!occupiedStorageSites.contains( + std::tie(nearestSLM, rLowNew, c))) { const auto distance = static_cast(architecture_.get().distance( previousSLM, previousRow, previousCol, nearestSLM, rLowNew, @@ -957,8 +880,7 @@ auto AStarPlacer::placeAtomsInStorageZone( assert(rHighNew - rHigh == 1); for (size_t c = cLowNew; c < cHighNew; ++c) { // NOTE: we have to use rHighNew - 1 here, which is equal to rHigh - if (occupiedStorageSites.find(std::tie(nearestSLM, rHigh, c)) == - occupiedStorageSites.end()) { + if (!occupiedStorageSites.contains(std::tie(nearestSLM, rHigh, c))) { const auto distance = static_cast(architecture_.get().distance( previousSLM, previousRow, previousCol, nearestSLM, rHigh, @@ -974,8 +896,8 @@ auto AStarPlacer::placeAtomsInStorageZone( if (cLowNew < cLow) { assert(cLow - cLowNew == 1); for (size_t r = rLow; r < rHigh; ++r) { - if (occupiedStorageSites.find(std::tie(nearestSLM, r, cLowNew)) == - occupiedStorageSites.end()) { + if (!occupiedStorageSites.contains( + std::tie(nearestSLM, r, cLowNew))) { const auto distance = static_cast(architecture_.get().distance( previousSLM, previousRow, previousCol, nearestSLM, r, @@ -993,8 +915,7 @@ auto AStarPlacer::placeAtomsInStorageZone( assert(cHighNew - cHigh == 1); for (size_t r = rLow; r < rHigh; ++r) { // NOTE: we have to use cHighNew - 1 here, which is equal to cHigh - if (occupiedStorageSites.find(std::tie(nearestSLM, r, cHigh)) == - occupiedStorageSites.end()) { + if (!occupiedStorageSites.contains(std::tie(nearestSLM, r, cHigh))) { const auto distance = static_cast(architecture_.get().distance( previousSLM, previousRow, previousCol, nearestSLM, r, @@ -1013,8 +934,8 @@ auto AStarPlacer::placeAtomsInStorageZone( cLow = cLowNew; cHigh = cHighNew; } - std::sort( - job.options.begin(), job.options.end(), + std::ranges::sort( + job.options, [](const AtomJob::Option& lhs, const AtomJob::Option& rhs) -> bool { return lhs.distance < rhs.distance; }); @@ -1063,39 +984,34 @@ auto AStarPlacer::placeAtomsInStorageZone( assert(!discreteRows.empty()); // ==> the following std::max_element does not // return a nullptr const uint8_t maxDiscreteSourceRow = - std::max_element(discreteRows.begin(), discreteRows.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }) - ->second; + std::ranges::max_element(discreteRows, [](const auto& lhs, + const auto& rhs) { + return lhs.second < rhs.second; + })->second; assert( !discreteColumns.empty()); // ==> the following std::max_element does not // return a nullptr const uint8_t maxDiscreteSourceColumn = - std::max_element(discreteColumns.begin(), discreteColumns.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }) - ->second; + std::ranges::max_element(discreteColumns, [](const auto& lhs, + const auto& rhs) { + return lhs.second < rhs.second; + })->second; assert(!discreteTargetRows .empty()); // ==> the following std::max_element does not // return a nullptr const uint8_t maxDiscreteTargetRow = - std::max_element(discreteTargetRows.begin(), discreteTargetRows.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }) - ->second; + std::ranges::max_element(discreteTargetRows, [](const auto& lhs, + const auto& rhs) { + return lhs.second < rhs.second; + })->second; assert(!discreteTargetColumns .empty()); // ==> the following std::max_element does not // return a nullptr const uint8_t maxDiscreteTargetColumn = - std::max_element(discreteTargetColumns.begin(), - discreteTargetColumns.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }) - ->second; + std::ranges::max_element(discreteTargetColumns, [](const auto& lhs, + const auto& rhs) { + return lhs.second < rhs.second; + })->second; const std::array scaleFactors{ std::min(1.F, static_cast(1 + maxDiscreteTargetRow) / static_cast(1 + maxDiscreteSourceRow)), @@ -1106,46 +1022,66 @@ auto AStarPlacer::placeAtomsInStorageZone( static_cast(1 + maxDiscreteTargetColumn) / static_cast(1 + maxDiscreteSourceColumn))}; //===------------------------------------------------------------------===// - // Run the A* algorithm + // Run the DFS algorithm //===------------------------------------------------------------------===// /** * A list of all nodes that have been created so far. * This happens when a node is expanded by calling getNeighbors. */ - std::deque> nodes; - nodes.emplace_back(std::make_unique()); const auto deepeningFactor = config_.deepeningFactor; const auto deepeningValue = config_.deepeningValue; - const auto& path = aStarTreeSearch( - *nodes.front(), - [&nodes, &atomJobs](const auto& node) { - return getNeighbors(nodes, atomJobs, std::move(node)); - }, - [nJobs](const auto& node) { return isGoal(nJobs, std::move(node)); }, - [](const auto& node) { return getCost(std::move(node)); }, - [&atomJobs, deepeningFactor, deepeningValue, - &scaleFactors](const auto& node) { - return getHeuristic(atomJobs, deepeningFactor, deepeningValue, - scaleFactors, std::move(node)); - }, - config_.maxNodes); + + std::shared_ptr node; + switch (config_.method) { + case Config::Method::IDS: + node = iterativeDivingSearch( + std::make_shared(), + [&atomJobs](const auto& node) { return getNeighbors(atomJobs, node); }, + [nJobs](const auto& node) { return isGoal(nJobs, node); }, + [](const auto& node) { return getCost(node); }, + [&atomJobs, deepeningFactor, deepeningValue, + &scaleFactors](const auto& node) { + return getHeuristic(atomJobs, deepeningFactor, deepeningValue, + scaleFactors, node); + }, + config_.trials, config_.queueCapacity); + break; + case Config::Method::ASTAR: + node = aStarTreeSearch( + std::make_shared(), + [&atomJobs](const auto& node) { return getNeighbors(atomJobs, node); }, + [nJobs](const auto& node) { return isGoal(nJobs, node); }, + [](const auto& node) { return getCost(node); }, + [&atomJobs, deepeningFactor, deepeningValue, + &scaleFactors](const auto& node) { + return getHeuristic(atomJobs, deepeningFactor, deepeningValue, + scaleFactors, node); + }, + config_.maxNodes); + break; + default: + qc::unreachable(); + } //===------------------------------------------------------------------===// // Extract the final mapping //===------------------------------------------------------------------===// - assert(path.size() == nJobs + 1); - for (size_t i = 0; i < nJobs; ++i) { - const auto& job = atomJobs[i]; - const auto& option = job.options[path[i + 1].get().option]; + while (node->parent != nullptr) { + assert(0 < node->level); + assert(node->level <= atomJobs.size()); + const auto& job = atomJobs[node->level - 1]; + assert(node->option < job.options.size()); + const auto& option = job.options[node->option]; if (!option.reuse) { const auto atom = job.atom; const auto& [row, col] = option.site; currentPlacement[atom] = targetSites.at(row).at(col); } + node = node->parent; } return currentPlacement; } -auto AStarPlacer::getCost(const GateNode& node) -> float { +auto HeuristicPlacer::getCost(const GateNode& node) -> float { float cost = node.lookaheadCost; for (const auto d : node.maxDistancesOfPlacedAtomsPerGroup) { cost += std::sqrt(d); @@ -1153,7 +1089,7 @@ auto AStarPlacer::getCost(const GateNode& node) -> float { return cost; } -auto AStarPlacer::getCost(const AtomNode& node) -> float { +auto HeuristicPlacer::getCost(const AtomNode& node) -> float { float cost = node.lookaheadCost; for (const auto d : node.maxDistancesOfPlacedAtomsPerGroup) { cost += std::sqrt(d); @@ -1161,7 +1097,7 @@ auto AStarPlacer::getCost(const AtomNode& node) -> float { return cost; } -auto AStarPlacer::sumStdDeviationForGroups( +auto HeuristicPlacer::sumStdDeviationForGroups( const std::array& scaleFactors, const std::vector& groups) -> float { float sumStdDev = 0.F; @@ -1188,11 +1124,11 @@ auto AStarPlacer::sumStdDeviationForGroups( return sumStdDev; } -auto AStarPlacer::getHeuristic(const std::vector& atomJobs, - const float deepeningFactor, - const float deepeningValue, - const std::array& scaleFactors, - const AtomNode& node) -> float { +auto HeuristicPlacer::getHeuristic(const std::vector& atomJobs, + const float deepeningFactor, + const float deepeningValue, + const std::array& scaleFactors, + const AtomNode& node) -> float { const auto nAtomJobs = atomJobs.size(); const auto nUnplacedAtoms = static_cast(nAtomJobs - node.level); float maxDistanceOfUnplacedAtom = 0.0F; @@ -1207,8 +1143,7 @@ auto AStarPlacer::getHeuristic(const std::vector& atomJobs, // first one for atoms that may be reused break; } - if (node.consumedFreeSites.find(option.site) == - node.consumedFreeSites.end()) { + if (!node.consumedFreeSites.contains(option.site)) { // this assumes that the first found free site is the nearest free site // for that atom. This requires that the job options are sorted by // distance. @@ -1234,11 +1169,11 @@ auto AStarPlacer::getHeuristic(const std::vector& atomJobs, return heuristic; } -auto AStarPlacer::getHeuristic(const std::vector& gateJobs, - const float deepeningFactor, - const float deepeningValue, - const std::array& scaleFactors, - const GateNode& node) -> float { +auto HeuristicPlacer::getHeuristic(const std::vector& gateJobs, + const float deepeningFactor, + const float deepeningValue, + const std::array& scaleFactors, + const GateNode& node) -> float { const auto nGateJobs = gateJobs.size(); const auto nUnplacedGates = static_cast(nGateJobs - node.level); float maxDistanceOfUnplacedAtom = 0.0F; @@ -1250,15 +1185,13 @@ auto AStarPlacer::getHeuristic(const std::vector& gateJobs, // this assumes that the first found pair of free sites is the nearest // pair of free sites for that gate. This requires that the job options // are sorted by distance. - if (std::all_of(option.sites.cbegin(), option.sites.cend(), - [&node](const DiscreteSite& site) -> bool { - return node.consumedFreeSites.find(site) == - node.consumedFreeSites.end(); - })) { + if (std::ranges::all_of(option.sites, + [&node](const DiscreteSite& site) -> bool { + return !node.consumedFreeSites.contains(site); + })) { maxDistanceOfUnplacedAtom = std::max(maxDistanceOfUnplacedAtom, - *std::max_element(option.distance.cbegin(), - option.distance.cend())); + *std::ranges::max_element(option.distance)); break; // exit when the first free site pair is found } } @@ -1279,24 +1212,24 @@ auto AStarPlacer::getHeuristic(const std::vector& gateJobs, return heuristic; } -auto AStarPlacer::getNeighbors(std::deque>& nodes, - const std::vector& atomJobs, - const AtomNode& node) - -> std::vector> { - const size_t atomToBePlacedNext = node.level; +auto HeuristicPlacer::getNeighbors(const std::vector& atomJobs, + const std::shared_ptr& node) + -> std::vector> { + const size_t atomToBePlacedNext = node->level; + assert(atomToBePlacedNext < atomJobs.size()); const auto& atomJob = atomJobs[atomToBePlacedNext]; - std::vector> neighbors; + std::vector> neighbors; assert(atomJob.options.size() <= std::numeric_limits::max()); for (uint16_t i = 0; i < static_cast(atomJob.options.size()); ++i) { const auto& option = atomJob.options[i]; const auto& [site, reuse, distance, lookaheadCost] = option; // skip the sites that are already consumed - if (!reuse && - node.consumedFreeSites.find(site) != node.consumedFreeSites.end()) { + if (!reuse && node->consumedFreeSites.contains(site)) { continue; } - // make a copy of node, the parent of child - AtomNode& child = *nodes.emplace_back(std::make_unique(node)); + // make a copy of the node, the parent of the child + AtomNode child = *node; // make a copy + child.parent = node; if (!reuse) { child.consumedFreeSites.emplace(site); // check whether the current placement is compatible with any existing @@ -1310,18 +1243,18 @@ auto AStarPlacer::getNeighbors(std::deque>& nodes, ++child.level; child.lookaheadCost += lookaheadCost; // add the child to the list of children to be returned - neighbors.emplace_back(child); + neighbors.emplace_back(std::make_shared(std::move(child))); } return neighbors; } -auto AStarPlacer::getNeighbors(std::deque>& nodes, - const std::vector& gateJobs, - const GateNode& node) - -> std::vector> { - const size_t gateToBePlacedNext = node.level; +auto HeuristicPlacer::getNeighbors(const std::vector& gateJobs, + const std::shared_ptr& node) + -> std::vector> { + const size_t gateToBePlacedNext = node->level; + assert(gateToBePlacedNext < gateJobs.size()); const auto& gateJob = gateJobs[gateToBePlacedNext]; - std::vector> neighbors; + std::vector> neighbors; // Get the current placement of the atoms that must be placed next const auto& [currentSiteOfLeftAtom, currentSiteOfRightAtom] = gateJob.currentSites; @@ -1331,14 +1264,14 @@ auto AStarPlacer::getNeighbors(std::deque>& nodes, const auto& [sites, distances, lookaheadCost] = option; const auto& [leftSite, rightSite] = sites; // skip if one of the sites is already consumed - if (node.consumedFreeSites.find(leftSite) != node.consumedFreeSites.end() || - node.consumedFreeSites.find(rightSite) != - node.consumedFreeSites.end()) { + if (node->consumedFreeSites.contains(leftSite) || + node->consumedFreeSites.contains(rightSite)) { continue; } - // make a copy of node, the parent of child as use this as a starting - // point for the new node - GateNode& child = *nodes.emplace_back(std::make_unique(node)); + // make a copy of the node, the parent of the child as use this as a + // starting point for the new node + GateNode child = *node; // make a copy + child.parent = node; ++child.level; child.option = i; child.consumedFreeSites.emplace(leftSite); @@ -1355,12 +1288,12 @@ auto AStarPlacer::getNeighbors(std::deque>& nodes, child.groups, child.maxDistancesOfPlacedAtomsPerGroup); child.lookaheadCost += lookaheadCost; // add the final child to the list of children to be returned - neighbors.emplace_back(child); + neighbors.emplace_back(std::make_shared(std::move(child))); } return neighbors; } -auto AStarPlacer::checkCompatibilityWithGroup( +auto HeuristicPlacer::checkCompatibilityWithGroup( const uint8_t key, const uint8_t value, const std::map& group) -> std::optional< @@ -1369,7 +1302,8 @@ auto AStarPlacer::checkCompatibilityWithGroup( // an assignment for this key already exists in this group if (const auto& [upperKey, upperValue] = *it; upperKey == key) { if (upperValue == value) { - // new placement is compatible with this group and key already exists + // the new placement is compatible with this group and key already + // exists return std::pair{it, true}; } } else { @@ -1378,13 +1312,13 @@ auto AStarPlacer::checkCompatibilityWithGroup( // it can be safely decremented if (const auto lowerValue = std::prev(it)->second; lowerValue < value && value < upperValue) { - // new placement is compatible with this group + // the new placement is compatible with this group return std::pair{it, false}; } } else { // if (it == hGroup.begin()) if (value < upperValue) { - // new placement is compatible with this group + // the new placement is compatible with this group return std::pair{it, false}; } } @@ -1394,14 +1328,14 @@ auto AStarPlacer::checkCompatibilityWithGroup( // it can be safely decremented because the group must contain // at least one element if (const auto lowerValue = std::prev(it)->second; lowerValue < value) { - // new placement is compatible with this group + // the new placement is compatible with this group return std::pair{it, false}; } } return std::nullopt; } -auto AStarPlacer::checkCompatibilityAndAddPlacement( +auto HeuristicPlacer::checkCompatibilityAndAddPlacement( const uint8_t hKey, const uint8_t hValue, const uint8_t vKey, const uint8_t vValue, const float distance, std::vector& groups, std::vector& maxDistances) @@ -1416,7 +1350,7 @@ auto AStarPlacer::checkCompatibilityAndAddPlacement( checkCompatibilityWithGroup(vKey, vValue, vGroup)) { const auto& [hIt, hExists] = *hCompatible; const auto& [vIt, vExists] = *vCompatible; - // new placement is compatible with this group + // the new placement is compatible with this group if (!hExists) { hGroup.emplace_hint(hIt, hKey, hValue); } @@ -1429,7 +1363,7 @@ auto AStarPlacer::checkCompatibilityAndAddPlacement( } ++i; } - // no compatible group could be found and a new group is created + // no compatible group could be found, and a new group is created auto& [hGroup, vGroup] = groups.emplace_back(); hGroup.emplace(hKey, hValue); vGroup.emplace(vKey, vValue); @@ -1437,7 +1371,28 @@ auto AStarPlacer::checkCompatibilityAndAddPlacement( return false; } -AStarPlacer::AStarPlacer(const Architecture& architecture, const Config& config) +auto HeuristicPlacer::Config::createWithAStarDefaults() -> Config { + Config config; + config.method = Method::ASTAR; + config.deepeningFactor = 0.8F; + config.deepeningValue = 0.2F; + config.lookaheadFactor = 0.2F; + return config; +} +auto HeuristicPlacer::Config::createWithIDSDefaults() -> Config { + Config config; + config.method = Method::IDS; + config.deepeningFactor = 0.01F; + config.deepeningValue = 0.0F; + config.lookaheadFactor = 0.4F; + return config; +} +auto HeuristicPlacer::Config::createForMethod(const Method method) -> Config { + return method == Method::ASTAR ? createWithAStarDefaults() + : createWithIDSDefaults(); +} +HeuristicPlacer::HeuristicPlacer(const Architecture& architecture, + const Config& config) : architecture_(architecture), config_(config) { // get first storage SLM and first entanglement SLM const auto& firstStorageSLM = *architecture_.get().storageZones.front(); @@ -1446,7 +1401,7 @@ AStarPlacer::AStarPlacer(const Architecture& architecture, const Config& config) // check which side of the first storage SLM is closer to the entanglement // SLM if (firstStorageSLM.location.second < firstEntanglementSLM.location.second) { - // if the entanglement SLM is closer to the last row of the storage SLM + // if the entanglement SLM is closer to the last row of the storage, SLM // start initial placement of the atoms in the last row instead of the // first and hence revert initial placement reverseInitialPlacement_ = true; @@ -1455,7 +1410,7 @@ AStarPlacer::AStarPlacer(const Architecture& architecture, const Config& config) config_.windowRatio * static_cast(config_.windowMinWidth))); } -auto AStarPlacer::place( +auto HeuristicPlacer::place( const size_t nQubits, const std::vector& twoQubitGateLayers, const std::vector>& reuseQubits) @@ -1474,6 +1429,7 @@ auto AStarPlacer::place( : twoQubitGateLayers[layer + 1]); placement.emplace_back(gatePlacement); placement.emplace_back(qubitPlacement); + SPDLOG_DEBUG("Placed layer: {}", layer); } return placement; } diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 8a8b83e62..cc852904c 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -66,6 +66,7 @@ constexpr std::string_view strictRoutingAwareConfiguration = R"({ "windowMinWidth" : 4, "windowRatio" : 1.5, "windowShare" : 0.6, + "method" : "astar", "deepeningFactor" : 0.6, "deepeningValue" : 0.2, "lookaheadFactor": 0.2, @@ -87,6 +88,7 @@ constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ "windowMinWidth" : 4, "windowRatio" : 1.5, "windowShare" : 0.6, + "method" : "astar", "deepeningFactor" : 0.6, "deepeningValue" : 0.2, "lookaheadFactor": 0.2, @@ -98,6 +100,29 @@ constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ } } })"; +constexpr std::string_view fastRelaxedRoutingAwareConfiguration = R"({ + "logLevel" : 1, + "codeGeneratorConfig" : { + "warnUnsupportedGates" : false + }, + "layoutSynthesizerConfig" : { + "placerConfig" : { + "useWindow" : true, + "windowMinWidth" : 4, + "windowRatio" : 1.5, + "windowShare" : 0.6, + "method" : "ids", + "deepeningFactor" : 0.01, + "deepeningValue" : 0.0, + "lookaheadFactor": 0.4, + "reuseLevel": 5.0 + }, + "routerConfig" : { + "method" : "relaxed", + "preferSplit" : 0.0 + } + } +})"; #define COMPILER_TEST(test_name, compiler_type, config) \ TEST(test_name##Test, ConstructorWithoutConfig) { \ Architecture architecture( \ @@ -158,6 +183,8 @@ COMPILER_TEST(StrictRoutingAwareCompiler, RoutingAwareCompiler, strictRoutingAwareConfiguration); COMPILER_TEST(RelaxedRoutingAwareCompiler, RoutingAwareCompiler, relaxedRoutingAwareConfiguration); +COMPILER_TEST(FastRelaxedRoutingAwareCompiler, RoutingAwareCompiler, + fastRelaxedRoutingAwareConfiguration); // Tests that the bug described in issue // https://github.com/munich-quantum-toolkit/qmap/issues/727 is fixed. @@ -326,4 +353,138 @@ TEST(RoutingAwareCompilerTest, Issue792) { RoutingAwareCompiler compiler(arch, config); EXPECT_NO_THROW(std::ignore = compiler.compile(qc)); } + +constexpr std::string_view architectureSpecificationGraphstate = R"({ + "name": "full_compute_store_architecture", + "operation_duration": { + "rydberg_gate": 0.36, + "single_qubit_gate": 52, + "atom_transfer": 15 + }, + "operation_fidelity": { + "rydberg_gate": 0.995, + "single_qubit_gate": 0.9997, + "atom_transfer": 0.999 + }, + "qubit_spec": { + "T": 1.5e6 + }, + "storage_zones": [ + { + "zone_id": 0, + "slms": [ + { + "id": 0, + "site_separation": [3, 3], + "r": 20, + "c": 100, + "location": [0, 0] + } + ], + "offset": [0, 0], + "dimension": [297, 57] + } + ], + "entanglement_zones": [ + { + "zone_id": 0, + "slms": [ + { + "id": 1, + "site_separation": [12, 10], + "r": 7, + "c": 20, + "location": [35, 67] + }, + { + "id": 2, + "site_separation": [12, 10], + "r": 7, + "c": 20, + "location": [37, 67] + } + ], + "offset": [35, 67], + "dimension": [230, 60] + } + ], + "aods": [ + { + "id": 0, + "site_separation": 2, + "r": 100, + "c": 100 + } + ], + "arch_range": [ + [0, 0], + [297, 162] + ], + "rydberg_range": [ + [ + [30, 62], + [270, 132] + ] + ] +})"; + +constexpr std::string_view circuitGraphstate = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit[20] q; +bit[20] c; +h q[0]; +h q[1]; +h q[2]; +h q[3]; +h q[4]; +h q[5]; +h q[6]; +cz q[1], q[6]; +cz q[4], q[6]; +h q[7]; +h q[8]; +cz q[2], q[8]; +h q[9]; +cz q[2], q[9]; +h q[10]; +cz q[3], q[10]; +h q[11]; +cz q[5], q[11]; +h q[12]; +cz q[8], q[12]; +h q[13]; +cz q[0], q[13]; +cz q[4], q[13]; +h q[14]; +cz q[0], q[14]; +cz q[5], q[14]; +h q[15]; +cz q[1], q[15]; +h q[16]; +cz q[10], q[16]; +cz q[15], q[16]; +h q[17]; +cz q[3], q[17]; +cz q[9], q[17]; +h q[18]; +cz q[7], q[18]; +cz q[11], q[18]; +h q[19]; +cz q[7], q[19]; +cz q[12], q[19];)"; + +TEST(FastRoutingAwareCompilerTest, ImprovedLoadingStoring) { + const auto qc = qasm3::Importer::imports(circuitGraphstate.data()); + const auto arch = + Architecture::fromJSONString(architectureSpecificationGraphstate); + const RoutingAwareCompiler::Config config = { + .layoutSynthesizerConfig = + {.placerConfig = HeuristicPlacer::Config::createForMethod( + HeuristicPlacer::Config::Method::IDS), + .routerConfig = {.method = + IndependentSetRouter::Config::Method::RELAXED}}, + .codeGeneratorConfig = {.warnUnsupportedGates = false}}; + RoutingAwareCompiler compiler(arch, config); + EXPECT_NO_THROW(std::ignore = compiler.compile(qc)); +} } // namespace na::zoned diff --git a/test/na/zoned/test_a_star_placer.cpp b/test/na/zoned/test_heuristic_placer.cpp similarity index 83% rename from test/na/zoned/test_a_star_placer.cpp rename to test/na/zoned/test_heuristic_placer.cpp index 49337c7f3..fe38a0d90 100644 --- a/test/na/zoned/test_a_star_placer.cpp +++ b/test/na/zoned/test_heuristic_placer.cpp @@ -8,7 +8,7 @@ * Licensed under the MIT License */ -#include "na/zoned/layout_synthesizer/placer/AStarPlacer.hpp" +#include "na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp" #include #include @@ -45,6 +45,7 @@ constexpr std::string_view configJson = R"({ "windowMinWidth": 4, "windowRatio": 1.5, "windowShare": 0.6, + "method": "astar", "deepeningFactor": 0.6, "deepeningValue": 0.2, "lookaheadFactor": 0.2, @@ -53,12 +54,12 @@ constexpr std::string_view configJson = R"({ class AStarPlacerPlaceTest : public ::testing::Test { protected: Architecture architecture; - AStarPlacer::Config config; - AStarPlacer placer; + HeuristicPlacer::Config config; + HeuristicPlacer placer; AStarPlacerPlaceTest() : architecture(Architecture::fromJSONString(architectureJson)), config(nlohmann::json::parse(configJson) - .template get()), + .template get()), placer(architecture, config) {} }; TEST_F(AStarPlacerPlaceTest, Empty) { @@ -219,19 +220,20 @@ TEST_F(AStarPlacerPlaceTest, TwoTwoQubitLayerReuse) { EXPECT_EQ(std::get<1>(placement[2][1]), std::get<1>(placement[3][1])); EXPECT_EQ(std::get<2>(placement[2][1]), std::get<2>(placement[3][1])); } -TEST(AStarPlacerTest, NoSolution) { +TEST(HeuristicPlacerTest, NoSolution) { Architecture architecture(Architecture::fromJSONString(architectureJson)); - AStarPlacer::Config config = R"({ + HeuristicPlacer::Config config = R"({ "useWindow": true, "windowMinWidth": 0, "windowRatio": 1.0, "windowShare": 0.0, + "method": "astar", "deepeningFactor": 0.6, "deepeningValue": 0.2, "lookaheadFactor": 0.2, "reuseLevel": 5.0 })"_json; - AStarPlacer placer(architecture, config); + HeuristicPlacer placer(architecture, config); constexpr size_t nQubits = 2; EXPECT_THROW( std::ignore = placer.place( @@ -240,34 +242,36 @@ TEST(AStarPlacerTest, NoSolution) { std::vector>{}), std::runtime_error); } -TEST(AStarPlacerTest, LimitSpace) { +TEST(HeuristicPlacerTest, LimitSpace) { Architecture architecture(Architecture::fromJSONString(architectureJson)); - AStarPlacer placer(architecture, R"({ + HeuristicPlacer placer(architecture, R"({ "useWindow": true, "windowMinWidth": 4, "windowRatio": 1.5, "windowShare": 0.6, + "method": "astar", "deepeningFactor": 0.6, "deepeningValue": 0.2, "lookaheadFactor": 0.2, "reuseLevel": 5.0, "maxNodes": 2 })"_json); - constexpr size_t nQubits = 4; + constexpr size_t nQubits = 8; EXPECT_THROW(std::ignore = placer.place( nQubits, std::vector>>{ - {{0U, 1U}, {2U, 3U}}}, + {{0U, 1U}, {2U, 3U}, {4U, 5U}, {6U, 7U}}}, std::vector>{}), std::runtime_error); } -TEST(AStarPlacerTest, WindowExpansion) { +TEST(HeuristicPlacerTest, WindowExpansion) { Architecture architecture(Architecture::fromJSONString(architectureJson)); - AStarPlacer placer(architecture, R"({ + HeuristicPlacer placer(architecture, R"({ "useWindow": true, "windowMinWidth": 1, "windowRatio": 1.0, "windowShare": 1.0, + "method": "astar", "deepeningFactor": 0.6, "deepeningValue": 0.2, "lookaheadFactor": 0.2, @@ -280,7 +284,7 @@ TEST(AStarPlacerTest, WindowExpansion) { {{0U, 3U}, {1U, 2U}}}, std::vector>{})); } -TEST(AStarPlacerTest, InitialPlacementForTwoSLMs) { +TEST(HeuristicPlacerTest, InitialPlacementForTwoSLMs) { const auto architecture = Architecture::fromJSON(R"({ "name": "a_star_placer_architecture", "storage_zones": [{ @@ -303,7 +307,7 @@ TEST(AStarPlacerTest, InitialPlacementForTwoSLMs) { "aods":[{"id": 0, "site_separation": 2, "r": 20, "c": 20}], "rydberg_range": [[[5, 70], [55, 110]]] })"_json); - AStarPlacer placer(architecture, nlohmann::json::parse(configJson)); + HeuristicPlacer placer(architecture, nlohmann::json::parse(configJson)); constexpr size_t nQubits = 50; const auto& placement = placer.place( nQubits, std::vector>>{}, @@ -315,7 +319,7 @@ TEST(AStarPlacerTest, InitialPlacementForTwoSLMs) { ::testing::Field(&SLM::id, ::testing::Eq(1)), ::testing::Lt(18), ::testing::Lt(20))))); } -TEST(AStarPlacerTest, AStarSearch) { +TEST(HeuristicPlacerTest, AStarSearch) { // for testing purposes, we do not use the structure of nodes and just use // their respective address to identify a location in a 4x4 grid that looks // like the following, where the cost of each edge is 1: @@ -337,56 +341,59 @@ TEST(AStarPlacerTest, AStarSearch) { // β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”ŒGoal=┐ β”Œβ”€β”€β”€β”€β”€β” // β”‚ 12 β”œβ”€β”€β”€β”€β”€β†’ β”‚ 13 β”œβ”€β”€β”€β”€β”€β†’ β”‚ 14 β”œβ”€β”€β”€β”€β”€β†’ β”‚ 15 β”‚ // β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””=====β”˜ β””β”€β”€β”€β”€β”€β”˜ - const std::vector nodes(16); - std::unordered_map< - const AStarPlacer::AtomNode*, - std::vector>> - neighbors{{nodes.data(), {std::cref(nodes[1]), std::cref(nodes[4])}}, - {&nodes[1], {std::cref(nodes[2]), std::cref(nodes[5])}}, - {&nodes[2], {std::cref(nodes[3]), std::cref(nodes[6])}}, - {&nodes[3], {std::cref(nodes[7])}}, - {&nodes[4], {std::cref(nodes[5]), std::cref(nodes[8])}}, - {&nodes[5], {std::cref(nodes[6]), std::cref(nodes[9])}}, - {&nodes[6], {std::cref(nodes[7]), std::cref(nodes[10])}}, - {&nodes[7], {std::cref(nodes[11])}}, - {&nodes[8], {std::cref(nodes[9]), std::cref(nodes[12])}}, - {&nodes[9], {std::cref(nodes[10]), std::cref(nodes[13])}}, - {&nodes[10], {std::cref(nodes[11]), std::cref(nodes[14])}}, - {&nodes[11], {std::cref(nodes[15])}}, - {&nodes[12], {std::cref(nodes[13])}}, - {&nodes[13], {std::cref(nodes[14])}}, - {&nodes[14], {std::cref(nodes[15])}}, - {&nodes[15], {}}}; - const auto path = (AStarPlacer::aStarTreeSearch( + struct Node { + double distanceToGoal = 0; + std::vector> neighbors{}; + explicit Node(const double d) : distanceToGoal(d) {} + }; + const std::vector nodes{ + std::make_shared(std::hypot(2, 3)), + std::make_shared(std::hypot(1, 3)), + std::make_shared(std::hypot(0, 3)), + std::make_shared(std::hypot(1, 3)), + std::make_shared(std::hypot(2, 2)), + std::make_shared(std::hypot(1, 2)), + std::make_shared(std::hypot(0, 2)), + std::make_shared(std::hypot(1, 2)), + std::make_shared(std::hypot(2, 1)), + std::make_shared(std::hypot(1, 1)), + std::make_shared(std::hypot(0, 1)), + std::make_shared(std::hypot(1, 1)), + std::make_shared(std::hypot(2, 0)), + std::make_shared(std::hypot(1, 0)), + std::make_shared(std::hypot(0, 0)), + std::make_shared(std::hypot(1, 0)), + }; + nodes[0]->neighbors = {nodes[1], nodes[4]}; + nodes[1]->neighbors = {nodes[2], nodes[5]}; + nodes[2]->neighbors = {nodes[3], nodes[6]}; + nodes[3]->neighbors = {nodes[7]}; + nodes[4]->neighbors = {nodes[5], nodes[8]}; + nodes[5]->neighbors = {nodes[6], nodes[9]}; + nodes[6]->neighbors = {nodes[7], nodes[10]}; + nodes[7]->neighbors = {nodes[11]}; + nodes[8]->neighbors = {nodes[9], nodes[12]}; + nodes[9]->neighbors = {nodes[10], nodes[13]}; + nodes[10]->neighbors = {nodes[11], nodes[14]}; + nodes[11]->neighbors = {nodes[15]}; + nodes[12]->neighbors = {nodes[13]}; + nodes[13]->neighbors = {nodes[14]}; + nodes[14]->neighbors = {nodes[15]}; + const auto goal = HeuristicPlacer::aStarTreeSearch( /* start: */ nodes[0], /* getNeighbors: */ - [&neighbors](const AStarPlacer::AtomNode& node) - -> std::vector> { - return neighbors.at(&node); + [](std::shared_ptr node) + -> std::vector> { + return node->neighbors; }, /* isGoal: */ - [&nodes](const AStarPlacer::AtomNode& node) -> bool { - return &node == &nodes[14]; - }, + [&nodes](const Node& node) -> bool { return &node == nodes[14].get(); }, /* getCost: */ - [](const AStarPlacer::AtomNode& /* unused */) -> double { return 1.0; }, + [](const Node& /* unused */) -> double { return 1.0; }, /* getHeuristic: */ - [&nodes](const AStarPlacer::AtomNode& node) -> double { - const auto* head = nodes.data(); - const auto i = std::distance(head, &node); - const long x = i % 4; - const long y = i / 4; - return std::hypot(x, y); - }, - 1'000'000)); - // convert to const Node* for easier comparison - std::vector pathNodes; - for (const auto& node : path) { - pathNodes.emplace_back(&node.get()); - } - EXPECT_THAT(pathNodes, - ::testing::ElementsAre(&nodes[0], ::testing::_, ::testing::_, - ::testing::_, ::testing::_, &nodes[14])); + [](const Node& node) -> double { return node.distanceToGoal; }, + 1'000'000); + EXPECT_EQ(goal, nodes[14]); } } // namespace na::zoned diff --git a/uv.lock b/uv.lock index 1e3b72bd2..e5bbe712a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.10, !=3.14.1" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", @@ -1603,7 +1603,8 @@ visualization = [ { name = "distinctipy" }, { name = "ipywidgets" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "plotly" }, { name = "walkerlayout" }, ] @@ -1621,7 +1622,8 @@ dev = [ { name = "mqt-core" }, { name = "mqt-qcec" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "nox" }, { name = "plotly" }, { name = "pybind11" }, @@ -1639,7 +1641,8 @@ docs = [ { name = "ipywidgets" }, { name = "myst-nb" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "plotly" }, { name = "qiskit", extra = ["qasm3-import", "visualization"] }, { name = "setuptools-scm" }, @@ -1658,7 +1661,8 @@ test = [ { name = "ipywidgets" }, { name = "mqt-qcec" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "plotly" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1843,7 +1847,6 @@ name = "networkx" version = "3.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", @@ -1853,6 +1856,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/c7/d64168da60332c17d24c0d2f08bdf3987e8d1ae9d84b5bbd0eec2eb26a55/networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f", size = 2063713, upload-time = "2025-11-24T03:03:45.21Z" }, ] +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + [[package]] name = "nox" version = "2025.11.12"