Skip to content

Commit ab83069

Browse files
ystadeCopilotpre-commit-ci[bot]denialhaag
committed
✨ [NA] Add IDS Placement Method (#862)
## Description ### Summary by CodeRabbit #### New Features - Introduced PlacementMethod configuration enum with astar and ids placement algorithm options - Added placement_method parameter to RoutingAwareCompiler for flexible algorithm selection during compilation #### Tests - Extended test coverage with new placement configuration scenarios and comprehensive test cases ### Walkthrough This PR refactors the quantum placement system by replacing AStarPlacer with HeuristicPlacer, introducing a configurable placement method enum (ASTAR and IDS) exposed to Python, modernizing method signatures to use shared_ptr-based nodes, and updating the placer configuration pipeline to accept placement method parameters in both C++ and Python interfaces. ## Checklist: <!--- This checklist serves as a reminder of a couple of things that ensure your pull request will be merged swiftly. --> - [x] The pull request only contains commits that are focused and relevant to this change. - [x] I have added appropriate tests that cover the new/changed functionality. - [x] I have updated the documentation to reflect these changes. - [x] I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals. - [ ] I have added migration instructions to the upgrade guide (if needed). - [x] The changes follow the project's style guidelines and introduce no new warnings. - [x] The changes are fully tested and pass the CI checks. - [x] I have reviewed my own code changes. --------- Signed-off-by: Yannick Stade <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Haag <[email protected]> # Conflicts: # include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp
1 parent a0f8ca9 commit ab83069

File tree

3 files changed

+71
-43
lines changed

3 files changed

+71
-43
lines changed

include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class HeuristicPlacer : public PlacerBase {
7070
size_t windowMinHeight_;
7171

7272
public:
73-
/// The configuration of the A* placer
73+
/// The configuration of the heuristic placer
7474
struct Config {
7575
/**
7676
* @brief This flag indicates whether the placement should use a window when
@@ -155,9 +155,7 @@ class HeuristicPlacer : public PlacerBase {
155155
* @brief The maximum number of nodes that are allowed to be visited in the
156156
* A* search tree.
157157
* @detsils If this number is exceeded, the search is aborted and an error
158-
* is raised. In the current implementation, one node roughly consumes 140
159-
* Byte. Hence, allowing 10,000,000 nodes results in memory consumption of
160-
* about 2 GB plus the size of the rest of the data structures.
158+
* is raised.
161159
*/
162160
size_t maxNodes = 10'000'000;
163161
/**
@@ -378,6 +376,8 @@ class HeuristicPlacer : public PlacerBase {
378376
std::vector<Node*> maxHeap_;
379377
/// The maximum number of elements in the heap.
380378
SizeType heapCapacity_;
379+
/// The comparison functor used to determine priority.
380+
PriorityCompare compare_;
381381

382382
/**
383383
* Starts to establish the min-heap property from the given index upwards.
@@ -387,7 +387,7 @@ class HeuristicPlacer : public PlacerBase {
387387
assert(i < minHeap_.size());
388388
while (i > 0) {
389389
size_t parent = (i - 1) / 2;
390-
if (PriorityCompare{}(minHeap_[i]->value, minHeap_[parent]->value)) {
390+
if (compare_(minHeap_[i]->value, minHeap_[parent]->value)) {
391391
std::swap(minHeap_[i], minHeap_[parent]);
392392
minHeap_[i]->minHeapIndex = i;
393393
minHeap_[parent]->minHeapIndex = parent;
@@ -406,7 +406,7 @@ class HeuristicPlacer : public PlacerBase {
406406
assert(i < maxHeap_.size());
407407
while (i > 0) {
408408
size_t parent = (i - 1) / 2;
409-
if (PriorityCompare{}(maxHeap_[parent]->value, maxHeap_[i]->value)) {
409+
if (compare_(maxHeap_[parent]->value, maxHeap_[i]->value)) {
410410
std::swap(maxHeap_[i], maxHeap_[parent]);
411411
maxHeap_[i]->maxHeapIndex = i;
412412
maxHeap_[parent]->maxHeapIndex = parent;
@@ -428,13 +428,11 @@ class HeuristicPlacer : public PlacerBase {
428428
size_t smallest = i;
429429

430430
if (leftChild < minHeap_.size() &&
431-
PriorityCompare{}(minHeap_[leftChild]->value,
432-
minHeap_[smallest]->value)) {
431+
compare_(minHeap_[leftChild]->value, minHeap_[smallest]->value)) {
433432
smallest = leftChild;
434433
}
435434
if (rightChild < minHeap_.size() &&
436-
PriorityCompare{}(minHeap_[rightChild]->value,
437-
minHeap_[smallest]->value)) {
435+
compare_(minHeap_[rightChild]->value, minHeap_[smallest]->value)) {
438436
smallest = rightChild;
439437
}
440438
if (smallest != i) {
@@ -459,13 +457,11 @@ class HeuristicPlacer : public PlacerBase {
459457
size_t largest = i;
460458

461459
if (leftChild < maxHeap_.size() &&
462-
PriorityCompare{}(maxHeap_[largest]->value,
463-
maxHeap_[leftChild]->value)) {
460+
compare_(maxHeap_[largest]->value, maxHeap_[leftChild]->value)) {
464461
largest = leftChild;
465462
}
466463
if (rightChild < maxHeap_.size() &&
467-
PriorityCompare{}(maxHeap_[largest]->value,
468-
maxHeap_[rightChild]->value)) {
464+
compare_(maxHeap_[largest]->value, maxHeap_[rightChild]->value)) {
469465
largest = rightChild;
470466
}
471467
if (largest != i) {
@@ -493,7 +489,7 @@ class HeuristicPlacer : public PlacerBase {
493489
maxHeap_.reserve(heapCapacity_);
494490
}
495491
/**
496-
* @returns the top element of the stack.
492+
* @returns the top element of the priority queue.
497493
* @note If @ref empty returns `true`, calling this function is
498494
* undefined behavior.
499495
*/
@@ -502,7 +498,7 @@ class HeuristicPlacer : public PlacerBase {
502498
return minHeap_.front()->value;
503499
}
504500
/**
505-
* @returns the top element of the stack.
501+
* @returns the top element of the priority queue.
506502
* @note If @ref empty returns `true`, calling this function is
507503
* undefined behavior.
508504
*/
@@ -535,15 +531,26 @@ class HeuristicPlacer : public PlacerBase {
535531
std::swap(maxHeap_[i], maxHeap_.back());
536532
maxHeap_.pop_back();
537533
maxHeap_[i]->maxHeapIndex = i;
538-
heapifyMaxHeapDown(i);
534+
// Restoring heap property may require moving the swapped element up
535+
// or down.
536+
if (i > 0) {
537+
const size_t parent = (i - 1) / 2;
538+
if (compare_(maxHeap_[parent]->value, maxHeap_[i]->value)) {
539+
heapifyMaxHeapUp(i);
540+
} else {
541+
heapifyMaxHeapDown(i);
542+
}
543+
} else {
544+
heapifyMaxHeapDown(i);
545+
}
539546
}
540547
}
541548
assert(minHeap_.size() == maxHeap_.size());
542549
assert(minHeap_.size() <= heapCapacity_);
543550
}
544-
/// @returns `true` if the stack is empty.
551+
/// @returns `true` if the queue is empty.
545552
[[nodiscard]] auto empty() const -> bool { return minHeap_.empty(); }
546-
/// @brief Inserts an element at the top.
553+
/// @brief Inserts an element into the priority queue.
547554
auto push(ValueType&& value) -> void {
548555
assert(minHeap_.size() == maxHeap_.size());
549556
if (heapCapacity_ > 0) {
@@ -556,7 +563,7 @@ class HeuristicPlacer : public PlacerBase {
556563
} else {
557564
assert(minHeap_.size() == heapCapacity_);
558565
// if capacity is reached, only insert the value if smaller than max
559-
if (PriorityCompare{}(value, maxHeap_.front()->value)) {
566+
if (compare_(value, maxHeap_.front()->value)) {
560567
const auto i = maxHeap_.front()->minHeapIndex;
561568
assert(i < minHeap_.size());
562569
minHeap_[i] = std::make_unique<Node>(i, 0, std::move(value));
@@ -593,10 +600,12 @@ class HeuristicPlacer : public PlacerBase {
593600
* given node.
594601
* @param isGoal is a function to determine whether a given node is a goal
595602
* node.
596-
* @param getCost is a function returning the cost of a node in the graph.
603+
* @param getCost is a function returning the cost of a node in the graph. The
604+
* cost of the start node must be 0.
597605
* @param getHeuristic is a function returning the heuristic value of a given
598606
* node.
599-
* @param trials is the number of restarts IDS performs.
607+
* @param trials is the number of attempts to find a goal node that are
608+
* performed at most. This parameter must be greater than 0.
600609
* @param queueCapacity is the capacity of the queue used for the iterative
601610
* diving search. For the actual capacity, the current value of trial is
602611
* added.
@@ -611,6 +620,9 @@ class HeuristicPlacer : public PlacerBase {
611620
const std::function<double(const Node&)>& getCost,
612621
const std::function<double(const Node&)>& getHeuristic, size_t trials,
613622
const size_t queueCapacity) -> std::shared_ptr<const Node> {
623+
if (trials == 0) {
624+
throw std::invalid_argument("IDS requires trials >= 1");
625+
}
614626
struct Item {
615627
double priority; //< sum of cost and heuristic
616628
std::shared_ptr<const Node> node; //< pointer to the node
@@ -625,6 +637,8 @@ class HeuristicPlacer : public PlacerBase {
625637
return a.priority < b.priority;
626638
}
627639
};
640+
// Initial capacity accounts for shrinking: popAndShrink() decrements
641+
// capacity on each trial
628642
BoundedPriorityQueue<Item, ItemCompare> queue(queueCapacity + trials);
629643
std::optional<Item> goal;
630644
Item currentItem{getHeuristic(*start), start};
@@ -702,9 +716,6 @@ class HeuristicPlacer : public PlacerBase {
702716
* general graphs. This is because it does not keep track of visited nodes and
703717
* therefore cannot detect cycles. Also, for DAGs it may expand nodes multiple
704718
* times when they can be reached by different paths from the start node.
705-
* @note @p getHeuristic must be admissible, meaning that it never
706-
* overestimates the cost to reach the goal from the current node calculated
707-
* by @p getCost for every edge on the path.
708719
* @note The calling program has to make sure that the pointers passed to this
709720
* function are valid and that the iterators are not invalidated during the
710721
* search, e.g., by calling one of the passed functions like @p getNeighbors.
@@ -714,11 +725,11 @@ class HeuristicPlacer : public PlacerBase {
714725
* @param isGoal is a function that returns true if a node is one of
715726
* potentially multiple goals
716727
* @param getCost is a function that returns the total cost to reach that
717-
* particular node from the start node
728+
* particular node from the start node. The cost of the start node must be 0.
718729
* @param getHeuristic is a function that returns the heuristic cost from the
719730
* node to any goal.
720-
* @param maxNodes is the maximum number of held in the priority queue before
721-
* the search is aborted.
731+
* @param maxNodes is the maximum number of nodes held in the priority queue
732+
* before the search is aborted. This parameter must be greater than 0.
722733
* @return a vector of node references representing the path from the start to
723734
* a goal
724735
*/
@@ -731,6 +742,9 @@ class HeuristicPlacer : public PlacerBase {
731742
const std::function<double(const Node&)>& getCost,
732743
const std::function<double(const Node&)>& getHeuristic,
733744
size_t maxNodes) -> std::shared_ptr<const Node> {
745+
if (maxNodes == 0) {
746+
throw std::invalid_argument("`maxNodes` must be greater than 0");
747+
}
734748
//===--------------------------------------------------------------------===//
735749
// Setup open set structure
736750
//===--------------------------------------------------------------------===//
@@ -758,7 +772,7 @@ class HeuristicPlacer : public PlacerBase {
758772
//===--------------------------------------------------------------------===//
759773
// Perform A* search
760774
//===--------------------------------------------------------------------===//
761-
while (openSet.size() < maxNodes && !openSet.empty()) {
775+
while (!openSet.empty()) {
762776
auto itm = openSet.top();
763777
openSet.pop();
764778
// if a goal is reached, that is the shortest path to a goal under the
@@ -776,13 +790,12 @@ class HeuristicPlacer : public PlacerBase {
776790
openSet.emplace(cost + heuristic, neighbor);
777791
}
778792
}
779-
}
780-
if (openSet.size() >= maxNodes) {
781-
throw std::runtime_error(
782-
"Maximum number of nodes reached. Increase max_nodes or increase "
783-
"deepening_value and deepening_factor to reduce the number of "
784-
"explored "
785-
"nodes.");
793+
if (openSet.size() >= maxNodes) {
794+
throw std::runtime_error(
795+
"Maximum number of nodes reached. Increase max_nodes or increase "
796+
"deepening_value and deepening_factor to reduce the number of "
797+
"explored nodes.");
798+
}
786799
}
787800
throw std::runtime_error(
788801
"No path from start to any goal found. This may be caused by a too "

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ classifiers = [
4949
"Development Status :: 5 - Production/Stable",
5050
"Typing :: Typed",
5151
]
52-
requires-python = ">=3.10"
52+
requires-python = ">=3.10, != 3.14.1"
5353
dependencies = [
5454
"mqt.core~=3.3.3",
5555
"qiskit[qasm3-import]>=1.0.0",

uv.lock

Lines changed: 21 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)