From c20a7427a99ab0cae7eaea68fc7e2e10fc89ed09 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:53:39 +0100 Subject: [PATCH 001/119] =?UTF-8?q?=E2=9C=A8=20Add=20max=20filling=20facto?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 142 ++++++---- include/na/zoned/scheduler/ASAPScheduler.hpp | 22 +- python/mqt/qmap/na/zoned.pyi | 259 ++++++++++++++++++- src/na/zoned/scheduler/ASAPScheduler.cpp | 8 +- 4 files changed, 363 insertions(+), 68 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 0cb1e9904..2588957eb 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -51,28 +51,43 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { return self.exportNAVizMachine(); }); + //===--------------------------------------------------------------------===// + // Routing-agnostic Compiler + //===--------------------------------------------------------------------===// py::class_ routingAgnosticCompiler( m, "RoutingAgnosticCompiler"); - routingAgnosticCompiler.def( - py::init([](const na::zoned::Architecture& arch, - const std::string& logLevel, const bool useWindow, - const size_t windowSize, const bool dynamicPlacement, - const size_t parkingOffset, const bool warnUnsupportedGates) - -> na::zoned::RoutingAgnosticCompiler { - na::zoned::RoutingAgnosticCompiler::Config config; - config.logLevel = spdlog::level::from_str(logLevel); - config.layoutSynthesizerConfig.placerConfig = {.useWindow = useWindow, - .windowSize = windowSize, - .dynamicPlacement = - dynamicPlacement}; - config.codeGeneratorConfig = {.parkingOffset = parkingOffset, - .warnUnsupportedGates = - warnUnsupportedGates}; - return {arch, config}; - }), - py::keep_alive<1, 2>(), "arch"_a, "log_level"_a = "warning", - "use_window"_a = true, "window_size"_a = 10, "dynamic_placement"_a = true, - "parking_offset"_a = 1, "warn_unsupported_gates"_a = true); + { + const na::zoned::RoutingAgnosticCompiler::Config defaultConfig; + routingAgnosticCompiler.def( + py::init([](const na::zoned::Architecture& arch, + const std::string& logLevel, const double maxFillingFactor, + const bool useWindow, const size_t windowSize, + const bool dynamicPlacement, + const bool warnUnsupportedGates) + -> na::zoned::RoutingAgnosticCompiler { + na::zoned::RoutingAgnosticCompiler::Config config; + config.logLevel = spdlog::level::from_str(logLevel); + config.schedulerConfig.maxFillingFactor = maxFillingFactor; + config.layoutSynthesizerConfig.placerConfig = { + .useWindow = useWindow, + .windowSize = windowSize, + .dynamicPlacement = dynamicPlacement}; + 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, + "use_window"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.useWindow, + "window_size"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.windowSize, + "dynamic_placement"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.dynamicPlacement, + "warn_unsupported_gates"_a = + defaultConfig.codeGeneratorConfig.warnUnsupportedGates); + } routingAgnosticCompiler.def_static( "from_json_string", [](const na::zoned::Architecture& arch, @@ -96,40 +111,63 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { return self.getStatistics(); }); + //===--------------------------------------------------------------------===// + // Routing-aware Compiler + //===--------------------------------------------------------------------===// py::class_ routingAwareCompiler( m, "RoutingAwareCompiler"); - routingAwareCompiler.def( - py::init([](const na::zoned::Architecture& arch, - const std::string& logLevel, 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 size_t parkingOffset, const bool warnUnsupportedGates) - -> na::zoned::RoutingAwareCompiler { - na::zoned::RoutingAwareCompiler::Config config; - config.logLevel = spdlog::level::from_str(logLevel); - config.layoutSynthesizerConfig.placerConfig = { - .useWindow = useWindow, - .windowMinWidth = windowMinWidth, - .windowRatio = windowRatio, - .windowShare = windowShare, - .deepeningFactor = deepeningFactor, - .deepeningValue = deepeningValue, - .lookaheadFactor = lookaheadFactor, - .reuseLevel = reuseLevel, - .maxNodes = maxNodes}; - config.codeGeneratorConfig = {.parkingOffset = parkingOffset, - .warnUnsupportedGates = - warnUnsupportedGates}; - return {arch, config}; - }), - py::keep_alive<1, 2>(), "arch"_a, "log_level"_a = "warning", - "use_window"_a = true, "window_min_width"_a = 8, "window_ratio"_a = 1.0, - "window_share"_a = 0.6, "deepening_factor"_a = 0.8, - "deepening_value"_a = 0.2, "lookahead_factor"_a = 0.2, - "reuse_level"_a = 5.0, "max_nodes"_a = 50000000, "parking_offset"_a = 1, - "warn_unsupported_gates"_a = true); + { + 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 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.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, + "use_window"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.useWindow, + "window_min_width"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.windowMinWidth, + "window_ratio"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.windowRatio, + "window_share"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.windowShare, + "deepening_factor"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.deepeningFactor, + "deepening_value"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.deepeningValue, + "lookahead_factor"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.lookaheadFactor, + "reuse_level"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.reuseLevel, + "max_nodes"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.maxNodes, + "warn_unsupported_gates"_a = + defaultConfig.codeGeneratorConfig.warnUnsupportedGates); + } routingAwareCompiler.def_static( "from_json_string", [](const na::zoned::Architecture& arch, diff --git a/include/na/zoned/scheduler/ASAPScheduler.hpp b/include/na/zoned/scheduler/ASAPScheduler.hpp index 851735f10..021b93ba6 100644 --- a/include/na/zoned/scheduler/ASAPScheduler.hpp +++ b/include/na/zoned/scheduler/ASAPScheduler.hpp @@ -34,24 +34,24 @@ class ASAPScheduler : public SchedulerBase { size_t maxTwoQubitGateNumPerLayer_ = 0; public: - /** - * The configuration of the ASAPScheduler - * @note ASAPScheduler does not have any configuration parameters. - */ + /// The configuration of the ASAPScheduler struct Config { - template - friend void to_json(BasicJsonType& /* unused */, - const Config& /* unused */) {} - template - friend void from_json(const BasicJsonType& /* unused */, - Config& /* unused */) {} + /// The maximal share of traps that are used in the entanglement zone. + double maxFillingFactor = 0.9; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, maxFillingFactor); }; + +private: + /// The configuration of the ASAPScheduler + Config config_; + +public: /** * Create a new ASAPScheduler. * @note The second parameter of the constructor is unused. * @param architecture is the architecture of the neutral atom system */ - ASAPScheduler(const Architecture& architecture, const Config& /* unused */); + ASAPScheduler(const Architecture& architecture, const Config& config); /** * This function schedules the operations of a quantum computation. * @details Every operation is scheduled as soon as possible. The function diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index b3710640f..bdd79b7cf 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -59,6 +59,7 @@ class RoutingAgnosticCompiler: self, arch: ZonedNeutralAtomArchitecture, log_level: str = ..., + max_filling_factor: float = ..., use_window: bool = ..., window_size: int = ..., dynamic_placement: bool = ..., @@ -71,6 +72,7 @@ class RoutingAgnosticCompiler: arch: is the zoned neutral atom architecture log_level: is the log level for the compiler, possible values are "debug", "info", "warning", "error", "critical" + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel use_window: whether to use a window for the placer window_size: the size of the window for the placer dynamic_placement: whether to use dynamic placement for the placer @@ -79,7 +81,7 @@ class RoutingAgnosticCompiler: """ @classmethod def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAgnosticCompiler: - """Create a routing-agnostic compiler for the given architecture and with configurations from a JSON string. + """Create a compiler for the given architecture and with configurations from a JSON string. Args: arch: is the zoned neutral atom architecture @@ -114,6 +116,7 @@ class RoutingAwareCompiler: self, arch: ZonedNeutralAtomArchitecture, log_level: str = ..., + max_filling_factor: float = ..., use_window: bool = ..., window_min_width: int = ..., window_ratio: float = ..., @@ -132,6 +135,7 @@ class RoutingAwareCompiler: arch: is the zoned neutral atom architecture log_level: is the log level for the compiler, possible values are "debug", "info", "warning", "error", "critical" + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel use_window: is a flag whether to use a window for the placer window_min_width: is the minimum width of the window for the placer window_ratio: is the ratio between the height and the width of the window @@ -160,7 +164,258 @@ class RoutingAwareCompiler: """ @classmethod def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAwareCompiler: - """Create a routing-aware compiler for the given architecture and configurations from a JSON string. + """Create a compiler for the given architecture and configurations from a JSON string. + + Args: + arch: is the zoned neutral atom architecture + json: is the JSON string + + Returns: + the initialized compiler + + Raises: + ValueError: if the string is not a valid JSON + """ + def compile(self, qc: QuantumComputation) -> str: + """Compile a quantum circuit for the zoned neutral atom architecture. + + Args: + qc: is the quantum circuit + + Returns: + the compilations result as a string in the .naviz format. + """ + def stats(self) -> dict[str, float]: + """Get the statistics of the last compilation. + + Returns: + the statistics as a dictionary + """ + +class FastRoutingAwareCompiler: + """MQT QMAP's fast, routing-aware Zoned Neutral Atom Compiler.""" + + def __init__( + self, + arch: ZonedNeutralAtomArchitecture, + log_level: str = ..., + max_filling_factor: float = ..., + use_window: bool = ..., + window_min_width: int = ..., + window_ratio: float = ..., + window_share: float = ..., + deepening_factor: float = ..., + deepening_value: float = ..., + lookahead_factor: float = ..., + reuse_level: float = ..., + trials: int = ..., + queue_capacity: int = ..., + parking_offset: int = ..., + warn_unsupported_gates: bool = ..., + ) -> None: + """Create a fast routing-aware compiler for the given architecture and configurations. + + Args: + arch: is the zoned neutral atom architecture + log_level: is the log level for the compiler, possible values are + "debug", "info", "warning", "error", "critical" + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel + use_window: is a flag whether to use a window for the placer + window_min_width: is the minimum width of the window for the placer + window_ratio: is the ratio between the height and the width of the window + window_share: is the share of free sites in the window in relation to the + number of atoms to be moved in this step + deepening_factor: controls the impact of the term in the heuristic of the + A* search that resembles the standard deviation of the differences + between the current and target sites of the atoms to be moved in every + orientation + deepening_value: is added to the sum of standard deviations before it is + multiplied with the number of unplaced nodes and :attr:`deepening_factor` + lookahead_factor: controls the lookahead's influence that considers the + distance of atoms to their interaction partner in the next layer + reuse_level: is the reuse level that corresponds to the estimated extra + fidelity loss due to the extra trap transfers when the atom is not + reused and instead moved to the storage zone and back to the + entanglement zone + trials: is the number of trials to run the DFS within the placer + queue_capacity: is the capacity of the priority queue used in the placer + parking_offset: is the parking offset of the code generator + warn_unsupported_gates: is a flag whether to warn about unsupported gates + in the code generator + """ + @classmethod + def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> FastRoutingAwareCompiler: + """Create a compiler for the given architecture and configurations from a JSON string. + + Args: + arch: is the zoned neutral atom architecture + json: is the JSON string + + Returns: + the initialized compiler + + Raises: + ValueError: if the string is not a valid JSON + """ + def compile(self, qc: QuantumComputation) -> str: + """Compile a quantum circuit for the zoned neutral atom architecture. + + Args: + qc: is the quantum circuit + + Returns: + the compilations result as a string in the .naviz format. + """ + def stats(self) -> dict[str, float]: + """Get the statistics of the last compilation. + + Returns: + the statistics as a dictionary + """ + +class RelaxedRoutingAwareCompiler: + """MQT QMAP's relaxed, routing-aware Zoned Neutral Atom Compiler.""" + + def __init__( + self, + arch: ZonedNeutralAtomArchitecture, + log_level: str = ..., + max_filling_factor: float = ..., + use_window: bool = ..., + window_min_width: int = ..., + window_ratio: float = ..., + window_share: float = ..., + deepening_factor: float = ..., + deepening_value: float = ..., + lookahead_factor: float = ..., + reuse_level: float = ..., + max_nodes: int = ..., + prefer_split: float = ..., + parking_offset: int = ..., + warn_unsupported_gates: bool = ..., + ) -> None: + """Create a relaxed, routing-aware compiler for the given architecture and configurations. + + Args: + arch: is the zoned neutral atom architecture + log_level: is the log level for the compiler, possible values are + "debug", "info", "warning", "error", "critical" + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel + use_window: is a flag whether to use a window for the placer + window_min_width: is the minimum width of the window for the placer + window_ratio: is the ratio between the height and the width of the window + window_share: is the share of free sites in the window in relation to the + number of atoms to be moved in this step + deepening_factor: controls the impact of the term in the heuristic of the + A* search that resembles the standard deviation of the differences + between the current and target sites of the atoms to be moved in every + orientation + deepening_value: is added to the sum of standard deviations before it is + multiplied with the number of unplaced nodes and :attr:`deepening_factor` + lookahead_factor: controls the lookahead's influence that considers the + distance of atoms to their interaction partner in the next layer + reuse_level: is the reuse level that corresponds to the estimated extra + fidelity loss due to the extra trap transfers when the atom is not + reused and instead moved to the storage zone and back to the + entanglement zone + max_nodes: is the maximum number of nodes that are considered in the A* + search. If this number is exceeded, the search is aborted and an error + is raised. In the current implementation, one node roughly consumes 120 + Byte. Hence, allowing 50,000,000 nodes results in memory consumption of + about 6 GB plus the size of the rest of the data structures. + prefer_split: is the preference for splitting the rearrangements into + separate rearrangement steps + parking_offset: is the parking offset of the code generator + warn_unsupported_gates: is a flag whether to warn about unsupported gates + in the code generator + """ + @classmethod + def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> RelaxedRoutingAwareCompiler: + """Create a compiler for the given architecture and configurations from a JSON string. + + Args: + arch: is the zoned neutral atom architecture + json: is the JSON string + + Returns: + the initialized compiler + + Raises: + ValueError: if the string is not a valid JSON + """ + def compile(self, qc: QuantumComputation) -> str: + """Compile a quantum circuit for the zoned neutral atom architecture. + + Args: + qc: is the quantum circuit + + Returns: + the compilations result as a string in the .naviz format. + """ + def stats(self) -> dict[str, float]: + """Get the statistics of the last compilation. + + Returns: + the statistics as a dictionary + """ + +class FastRelaxedRoutingAwareCompiler: + """MQT QMAP's elastic Zoned Neutral Atom Compiler.""" + + def __init__( + self, + arch: ZonedNeutralAtomArchitecture, + log_level: str = ..., + max_filling_factor: float = ..., + use_window: bool = ..., + window_min_width: int = ..., + window_ratio: float = ..., + window_share: float = ..., + deepening_factor: float = ..., + deepening_value: float = ..., + lookahead_factor: float = ..., + reuse_level: float = ..., + trials: int = ..., + queue_capacity: int = ..., + prefer_split: float = ..., + parking_offset: int = ..., + warn_unsupported_gates: bool = ..., + ) -> None: + """Create a fast, relaxed, routing-aware compiler for the given architecture and configurations. + + Args: + arch: is the zoned neutral atom architecture + log_level: is the log level for the compiler, possible values are + "debug", "info", "warning", "error", "critical" + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel + use_window: is a flag whether to use a window for the placer + window_min_width: is the minimum width of the window for the placer + window_ratio: is the ratio between the height and the width of the window + window_share: is the share of free sites in the window in relation to the + number of atoms to be moved in this step + deepening_factor: controls the impact of the term in the heuristic of the + A* search that resembles the standard deviation of the differences + between the current and target sites of the atoms to be moved in every + orientation + deepening_value: is added to the sum of standard deviations before it is + multiplied with the number of unplaced nodes and :attr:`deepening_factor` + lookahead_factor: controls the lookahead's influence that considers the + distance of atoms to their interaction partner in the next layer + reuse_level: is the reuse level that corresponds to the estimated extra + fidelity loss due to the extra trap transfers when the atom is not + reused and instead moved to the storage zone and back to the + entanglement zone + trials: is the number of trials to run the DFS within the placer + queue_capacity: is the capacity of the priority queue used in the placer + prefer_split: is the preference for splitting the rearrangements into + separate rearrangement steps + parking_offset: is the parking offset of the code generator + warn_unsupported_gates: is a flag whether to warn about unsupported gates + in the code generator + """ + @classmethod + def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> FastRelaxedRoutingAwareCompiler: + """Create a compiler for the given architecture and configurations from a JSON string. Args: arch: is the zoned neutral atom architecture diff --git a/src/na/zoned/scheduler/ASAPScheduler.cpp b/src/na/zoned/scheduler/ASAPScheduler.cpp index de8ba14df..8106f3718 100644 --- a/src/na/zoned/scheduler/ASAPScheduler.cpp +++ b/src/na/zoned/scheduler/ASAPScheduler.cpp @@ -30,11 +30,13 @@ namespace na::zoned { ASAPScheduler::ASAPScheduler(const Architecture& architecture, - const Config& /* unused */) - : architecture_(architecture) { + const Config& config) + : architecture_(architecture), config_(config) { // calculate the maximum possible number of two-qubit gates per layer for (const auto& zone : architecture_.get().entanglementZones) { - maxTwoQubitGateNumPerLayer_ += zone->front().nRows * zone->front().nCols; + maxTwoQubitGateNumPerLayer_ += static_cast( + config_.maxFillingFactor * + static_cast(zone->front().nRows * zone->front().nCols)); } if (maxTwoQubitGateNumPerLayer_ == 0) { throw std::invalid_argument("Architecture must contain at least one site " From 1620370fac1824c493881a90886049a0ac9416f0 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:34:20 +0100 Subject: [PATCH 002/119] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20added=20?= =?UTF-8?q?behaviour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_asap_scheduler.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/na/zoned/test_asap_scheduler.cpp b/test/na/zoned/test_asap_scheduler.cpp index f8e3d266c..8d9078019 100644 --- a/test/na/zoned/test_asap_scheduler.cpp +++ b/test/na/zoned/test_asap_scheduler.cpp @@ -45,7 +45,7 @@ constexpr std::string_view architectureJson = R"({ class ASAPSchedulerScheduleTest : public ::testing::Test { protected: Architecture architecture; - nlohmann::json config; + ASAPScheduler::Config config{.maxFillingFactor = .8}; ASAPScheduler scheduler; ASAPSchedulerScheduleTest() : architecture(Architecture::fromJSONString(architectureJson)), @@ -213,4 +213,14 @@ TEST_F(ASAPSchedulerScheduleTest, UnsupportedCXGate) { qc.cx(0, 1); EXPECT_THROW(std::ignore = scheduler.schedule(qc), std::invalid_argument); } +TEST_F(ASAPSchedulerScheduleTest, FullEntanglementZone) { + qc::QuantumComputation qc(26); + for (qc::Qubit i = 0; i < 13; ++i) { + qc.cz(2 * i, 2 * i + 1); + } + const auto& [singleQubitGateLayers, twoQubitGateLayers] = + scheduler.schedule(qc); + EXPECT_THAT(singleQubitGateLayers, ::testing::SizeIs(3)); + EXPECT_THAT(twoQubitGateLayers, ::testing::SizeIs(2)); +} } // namespace na::zoned From ec0d8e81ed1c97a1a58b0b601faded5f6b11b482 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:57:23 +0100 Subject: [PATCH 003/119] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Yannick Stade <100073938+ystade@users.noreply.github.com> --- include/na/zoned/scheduler/ASAPScheduler.hpp | 2 +- python/mqt/qmap/na/zoned.pyi | 4 ++-- src/na/zoned/scheduler/ASAPScheduler.cpp | 14 +++++++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/include/na/zoned/scheduler/ASAPScheduler.hpp b/include/na/zoned/scheduler/ASAPScheduler.hpp index 021b93ba6..8c570b669 100644 --- a/include/na/zoned/scheduler/ASAPScheduler.hpp +++ b/include/na/zoned/scheduler/ASAPScheduler.hpp @@ -48,8 +48,8 @@ class ASAPScheduler : public SchedulerBase { public: /** * Create a new ASAPScheduler. - * @note The second parameter of the constructor is unused. * @param architecture is the architecture of the neutral atom system + * @param config is the configuration for the scheduler */ ASAPScheduler(const Architecture& architecture, const Config& config); /** diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index bdd79b7cf..143442d79 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -72,7 +72,7 @@ class RoutingAgnosticCompiler: arch: is the zoned neutral atom architecture log_level: is the log level for the compiler, possible values are "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel use_window: whether to use a window for the placer window_size: the size of the window for the placer dynamic_placement: whether to use dynamic placement for the placer @@ -135,7 +135,7 @@ class RoutingAwareCompiler: arch: is the zoned neutral atom architecture log_level: is the log level for the compiler, possible values are "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel use_window: is a flag whether to use a window for the placer window_min_width: is the minimum width of the window for the placer window_ratio: is the ratio between the height and the width of the window diff --git a/src/na/zoned/scheduler/ASAPScheduler.cpp b/src/na/zoned/scheduler/ASAPScheduler.cpp index 8106f3718..3bd97ca7e 100644 --- a/src/na/zoned/scheduler/ASAPScheduler.cpp +++ b/src/na/zoned/scheduler/ASAPScheduler.cpp @@ -32,11 +32,19 @@ namespace na::zoned { ASAPScheduler::ASAPScheduler(const Architecture& architecture, const Config& config) : architecture_(architecture), config_(config) { + // Validate maxFillingFactor + if (config_.maxFillingFactor <= 0.0 || config_.maxFillingFactor > 1.0) { + std::ostringstream oss; + oss << "Invalid maxFillingFactor: " << config_.maxFillingFactor + << ". Value must be in the range (0, 1.0]."; + throw std::invalid_argument(oss.str()); + } // calculate the maximum possible number of two-qubit gates per layer for (const auto& zone : architecture_.get().entanglementZones) { - maxTwoQubitGateNumPerLayer_ += static_cast( - config_.maxFillingFactor * - static_cast(zone->front().nRows * zone->front().nCols)); + maxTwoQubitGateNumPerLayer_ += std::max( + 1UL, + static_cast(config_.maxFillingFactor * + static_cast(zone->front().nRows * zone->front().nCols))); } if (maxTwoQubitGateNumPerLayer_ == 0) { throw std::invalid_argument("Architecture must contain at least one site " From c69f26e54a520bf777c840688f2716dd9ffce568 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:57:55 +0000 Subject: [PATCH 004/119] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/scheduler/ASAPScheduler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/na/zoned/scheduler/ASAPScheduler.cpp b/src/na/zoned/scheduler/ASAPScheduler.cpp index 3bd97ca7e..a743859f8 100644 --- a/src/na/zoned/scheduler/ASAPScheduler.cpp +++ b/src/na/zoned/scheduler/ASAPScheduler.cpp @@ -42,9 +42,9 @@ ASAPScheduler::ASAPScheduler(const Architecture& architecture, // calculate the maximum possible number of two-qubit gates per layer for (const auto& zone : architecture_.get().entanglementZones) { maxTwoQubitGateNumPerLayer_ += std::max( - 1UL, - static_cast(config_.maxFillingFactor * - static_cast(zone->front().nRows * zone->front().nCols))); + 1UL, static_cast(config_.maxFillingFactor * + static_cast(zone->front().nRows * + zone->front().nCols))); } if (maxTwoQubitGateNumPerLayer_ == 0) { throw std::invalid_argument("Architecture must contain at least one site " From 02528335ce3b87a9038e1595b8f1432b4d80aeb7 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:57:58 +0100 Subject: [PATCH 005/119] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20leftover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 251 ----------------------------------- 1 file changed, 251 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 143442d79..2cc32c74c 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -191,254 +191,3 @@ class RoutingAwareCompiler: Returns: the statistics as a dictionary """ - -class FastRoutingAwareCompiler: - """MQT QMAP's fast, routing-aware Zoned Neutral Atom Compiler.""" - - def __init__( - self, - arch: ZonedNeutralAtomArchitecture, - log_level: str = ..., - max_filling_factor: float = ..., - use_window: bool = ..., - window_min_width: int = ..., - window_ratio: float = ..., - window_share: float = ..., - deepening_factor: float = ..., - deepening_value: float = ..., - lookahead_factor: float = ..., - reuse_level: float = ..., - trials: int = ..., - queue_capacity: int = ..., - parking_offset: int = ..., - warn_unsupported_gates: bool = ..., - ) -> None: - """Create a fast routing-aware compiler for the given architecture and configurations. - - Args: - arch: is the zoned neutral atom architecture - log_level: is the log level for the compiler, possible values are - "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel - use_window: is a flag whether to use a window for the placer - window_min_width: is the minimum width of the window for the placer - window_ratio: is the ratio between the height and the width of the window - window_share: is the share of free sites in the window in relation to the - number of atoms to be moved in this step - deepening_factor: controls the impact of the term in the heuristic of the - A* search that resembles the standard deviation of the differences - between the current and target sites of the atoms to be moved in every - orientation - deepening_value: is added to the sum of standard deviations before it is - multiplied with the number of unplaced nodes and :attr:`deepening_factor` - lookahead_factor: controls the lookahead's influence that considers the - distance of atoms to their interaction partner in the next layer - reuse_level: is the reuse level that corresponds to the estimated extra - fidelity loss due to the extra trap transfers when the atom is not - reused and instead moved to the storage zone and back to the - entanglement zone - trials: is the number of trials to run the DFS within the placer - queue_capacity: is the capacity of the priority queue used in the placer - parking_offset: is the parking offset of the code generator - warn_unsupported_gates: is a flag whether to warn about unsupported gates - in the code generator - """ - @classmethod - def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> FastRoutingAwareCompiler: - """Create a compiler for the given architecture and configurations from a JSON string. - - Args: - arch: is the zoned neutral atom architecture - json: is the JSON string - - Returns: - the initialized compiler - - Raises: - ValueError: if the string is not a valid JSON - """ - def compile(self, qc: QuantumComputation) -> str: - """Compile a quantum circuit for the zoned neutral atom architecture. - - Args: - qc: is the quantum circuit - - Returns: - the compilations result as a string in the .naviz format. - """ - def stats(self) -> dict[str, float]: - """Get the statistics of the last compilation. - - Returns: - the statistics as a dictionary - """ - -class RelaxedRoutingAwareCompiler: - """MQT QMAP's relaxed, routing-aware Zoned Neutral Atom Compiler.""" - - def __init__( - self, - arch: ZonedNeutralAtomArchitecture, - log_level: str = ..., - max_filling_factor: float = ..., - use_window: bool = ..., - window_min_width: int = ..., - window_ratio: float = ..., - window_share: float = ..., - deepening_factor: float = ..., - deepening_value: float = ..., - lookahead_factor: float = ..., - reuse_level: float = ..., - max_nodes: int = ..., - prefer_split: float = ..., - parking_offset: int = ..., - warn_unsupported_gates: bool = ..., - ) -> None: - """Create a relaxed, routing-aware compiler for the given architecture and configurations. - - Args: - arch: is the zoned neutral atom architecture - log_level: is the log level for the compiler, possible values are - "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel - use_window: is a flag whether to use a window for the placer - window_min_width: is the minimum width of the window for the placer - window_ratio: is the ratio between the height and the width of the window - window_share: is the share of free sites in the window in relation to the - number of atoms to be moved in this step - deepening_factor: controls the impact of the term in the heuristic of the - A* search that resembles the standard deviation of the differences - between the current and target sites of the atoms to be moved in every - orientation - deepening_value: is added to the sum of standard deviations before it is - multiplied with the number of unplaced nodes and :attr:`deepening_factor` - lookahead_factor: controls the lookahead's influence that considers the - distance of atoms to their interaction partner in the next layer - reuse_level: is the reuse level that corresponds to the estimated extra - fidelity loss due to the extra trap transfers when the atom is not - reused and instead moved to the storage zone and back to the - entanglement zone - max_nodes: is the maximum number of nodes that are considered in the A* - search. If this number is exceeded, the search is aborted and an error - is raised. In the current implementation, one node roughly consumes 120 - Byte. Hence, allowing 50,000,000 nodes results in memory consumption of - about 6 GB plus the size of the rest of the data structures. - prefer_split: is the preference for splitting the rearrangements into - separate rearrangement steps - parking_offset: is the parking offset of the code generator - warn_unsupported_gates: is a flag whether to warn about unsupported gates - in the code generator - """ - @classmethod - def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> RelaxedRoutingAwareCompiler: - """Create a compiler for the given architecture and configurations from a JSON string. - - Args: - arch: is the zoned neutral atom architecture - json: is the JSON string - - Returns: - the initialized compiler - - Raises: - ValueError: if the string is not a valid JSON - """ - def compile(self, qc: QuantumComputation) -> str: - """Compile a quantum circuit for the zoned neutral atom architecture. - - Args: - qc: is the quantum circuit - - Returns: - the compilations result as a string in the .naviz format. - """ - def stats(self) -> dict[str, float]: - """Get the statistics of the last compilation. - - Returns: - the statistics as a dictionary - """ - -class FastRelaxedRoutingAwareCompiler: - """MQT QMAP's elastic Zoned Neutral Atom Compiler.""" - - def __init__( - self, - arch: ZonedNeutralAtomArchitecture, - log_level: str = ..., - max_filling_factor: float = ..., - use_window: bool = ..., - window_min_width: int = ..., - window_ratio: float = ..., - window_share: float = ..., - deepening_factor: float = ..., - deepening_value: float = ..., - lookahead_factor: float = ..., - reuse_level: float = ..., - trials: int = ..., - queue_capacity: int = ..., - prefer_split: float = ..., - parking_offset: int = ..., - warn_unsupported_gates: bool = ..., - ) -> None: - """Create a fast, relaxed, routing-aware compiler for the given architecture and configurations. - - Args: - arch: is the zoned neutral atom architecture - log_level: is the log level for the compiler, possible values are - "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it set the limit for the maximum number of entangling gates that are scheduled in parallel - use_window: is a flag whether to use a window for the placer - window_min_width: is the minimum width of the window for the placer - window_ratio: is the ratio between the height and the width of the window - window_share: is the share of free sites in the window in relation to the - number of atoms to be moved in this step - deepening_factor: controls the impact of the term in the heuristic of the - A* search that resembles the standard deviation of the differences - between the current and target sites of the atoms to be moved in every - orientation - deepening_value: is added to the sum of standard deviations before it is - multiplied with the number of unplaced nodes and :attr:`deepening_factor` - lookahead_factor: controls the lookahead's influence that considers the - distance of atoms to their interaction partner in the next layer - reuse_level: is the reuse level that corresponds to the estimated extra - fidelity loss due to the extra trap transfers when the atom is not - reused and instead moved to the storage zone and back to the - entanglement zone - trials: is the number of trials to run the DFS within the placer - queue_capacity: is the capacity of the priority queue used in the placer - prefer_split: is the preference for splitting the rearrangements into - separate rearrangement steps - parking_offset: is the parking offset of the code generator - warn_unsupported_gates: is a flag whether to warn about unsupported gates - in the code generator - """ - @classmethod - def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> FastRelaxedRoutingAwareCompiler: - """Create a compiler for the given architecture and configurations from a JSON string. - - Args: - arch: is the zoned neutral atom architecture - json: is the JSON string - - Returns: - the initialized compiler - - Raises: - ValueError: if the string is not a valid JSON - """ - def compile(self, qc: QuantumComputation) -> str: - """Compile a quantum circuit for the zoned neutral atom architecture. - - Args: - qc: is the quantum circuit - - Returns: - the compilations result as a string in the .naviz format. - """ - def stats(self) -> dict[str, float]: - """Get the statistics of the last compilation. - - Returns: - the statistics as a dictionary - """ From c94ac787d2e73fb1a521cea5ca4b1f3c5577cac6 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:16:50 +0100 Subject: [PATCH 006/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20missing=20parking?= =?UTF-8?q?=20offset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 2588957eb..7e8195174 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -62,7 +62,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { py::init([](const na::zoned::Architecture& arch, const std::string& logLevel, const double maxFillingFactor, const bool useWindow, const size_t windowSize, - const bool dynamicPlacement, + const bool dynamicPlacement, const size_t parkingOffset, const bool warnUnsupportedGates) -> na::zoned::RoutingAgnosticCompiler { na::zoned::RoutingAgnosticCompiler::Config config; @@ -72,7 +72,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { .useWindow = useWindow, .windowSize = windowSize, .dynamicPlacement = dynamicPlacement}; - config.codeGeneratorConfig = {.warnUnsupportedGates = + config.codeGeneratorConfig = {.parkingOffset = parkingOffset, + .warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; }), @@ -85,6 +86,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { defaultConfig.layoutSynthesizerConfig.placerConfig.windowSize, "dynamic_placement"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.dynamicPlacement, + "parking_offset"_a = defaultConfig.codeGeneratorConfig.parkingOffset, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } @@ -125,7 +127,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { const double windowRatio, const double windowShare, const float deepeningFactor, const float deepeningValue, const float lookaheadFactor, const float reuseLevel, - const size_t maxNodes, const bool warnUnsupportedGates) + const size_t maxNodes, const size_t parkingOffset, + const bool warnUnsupportedGates) -> na::zoned::RoutingAwareCompiler { na::zoned::RoutingAwareCompiler::Config config; config.logLevel = spdlog::level::from_str(logLevel); @@ -140,7 +143,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { .lookaheadFactor = lookaheadFactor, .reuseLevel = reuseLevel, .maxNodes = maxNodes}; - config.codeGeneratorConfig = {.warnUnsupportedGates = + config.codeGeneratorConfig = {.parkingOffset = parkingOffset, + .warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; }), @@ -165,6 +169,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { defaultConfig.layoutSynthesizerConfig.placerConfig.reuseLevel, "max_nodes"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.maxNodes, + "parking_offset"_a = defaultConfig.codeGeneratorConfig.parkingOffset, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } From 5ee5b7ca92a51656693910d5b9b10940d590749b Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:19:15 +0100 Subject: [PATCH 007/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20type=20conversion?= =?UTF-8?q?=20bug=20on=20windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/scheduler/ASAPScheduler.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/na/zoned/scheduler/ASAPScheduler.cpp b/src/na/zoned/scheduler/ASAPScheduler.cpp index a743859f8..ede0606bd 100644 --- a/src/na/zoned/scheduler/ASAPScheduler.cpp +++ b/src/na/zoned/scheduler/ASAPScheduler.cpp @@ -41,10 +41,11 @@ ASAPScheduler::ASAPScheduler(const Architecture& architecture, } // calculate the maximum possible number of two-qubit gates per layer for (const auto& zone : architecture_.get().entanglementZones) { - maxTwoQubitGateNumPerLayer_ += std::max( - 1UL, static_cast(config_.maxFillingFactor * - static_cast(zone->front().nRows * - zone->front().nCols))); + maxTwoQubitGateNumPerLayer_ += + std::max(static_cast(1U), + static_cast(config_.maxFillingFactor * + static_cast(zone->front().nRows * + zone->front().nCols))); } if (maxTwoQubitGateNumPerLayer_ == 0) { throw std::invalid_argument("Architecture must contain at least one site " From 708d11d3a124c513464b4ad490ca93a9f257aaf3 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:24:53 +0100 Subject: [PATCH 008/119] =?UTF-8?q?=E2=9C=85=20Increase=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_asap_scheduler.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/na/zoned/test_asap_scheduler.cpp b/test/na/zoned/test_asap_scheduler.cpp index 8d9078019..2c9c2c0ef 100644 --- a/test/na/zoned/test_asap_scheduler.cpp +++ b/test/na/zoned/test_asap_scheduler.cpp @@ -223,4 +223,13 @@ TEST_F(ASAPSchedulerScheduleTest, FullEntanglementZone) { EXPECT_THAT(singleQubitGateLayers, ::testing::SizeIs(3)); EXPECT_THAT(twoQubitGateLayers, ::testing::SizeIs(2)); } +TEST(ASAPSchedulerConfigTest, InvalidMaxFillingFactor) { + const auto architecture = Architecture::fromJSONString(architectureJson); + constexpr ASAPScheduler::Config config1{.maxFillingFactor = 0.}; + EXPECT_THROW(ASAPScheduler scheduler(architecture, config1), + std::invalid_argument); + constexpr ASAPScheduler::Config config2{.maxFillingFactor = 1.1}; + EXPECT_THROW(ASAPScheduler scheduler(architecture, config2), + std::invalid_argument); +} } // namespace na::zoned From a8c042c43ce08c81b5ba289635795e0d587953e7 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 09:18:38 +0100 Subject: [PATCH 009/119] =?UTF-8?q?=F0=9F=93=9D=20Add=20changelog=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1c7aef76..e66d7c5df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ## [Unreleased] +_If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ + +### Changed + +- ✨ Add `max_filling_factor` to Scheduler in Zoned Neutral Atom Compiler ([#847]) ([**@ystade**]) + ## [3.4.0] - 2025-10-15 _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#340)._ @@ -151,6 +157,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ +[#847]: https://github.com/munich-quantum-toolkit/qmap/pull/847 [#804]: https://github.com/munich-quantum-toolkit/qmap/pull/804 [#803]: https://github.com/munich-quantum-toolkit/qmap/pull/803 [#796]: https://github.com/munich-quantum-toolkit/qmap/pull/796 From 61688dd28e7437fe40f94523c29dce41f22a02d4 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 10:55:55 +0100 Subject: [PATCH 010/119] =?UTF-8?q?=E2=9C=A8=20Enable=20relaxed=20rearrang?= =?UTF-8?q?ements=20in=20code-generator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 13 +- .../na/zoned/code_generator/CodeGenerator.hpp | 106 +- src/na/zoned/code_generator/CodeGenerator.cpp | 999 ++++++++++++++++-- 3 files changed, 1044 insertions(+), 74 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 7e8195174..2588957eb 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -62,7 +62,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { py::init([](const na::zoned::Architecture& arch, const std::string& logLevel, const double maxFillingFactor, const bool useWindow, const size_t windowSize, - const bool dynamicPlacement, const size_t parkingOffset, + const bool dynamicPlacement, const bool warnUnsupportedGates) -> na::zoned::RoutingAgnosticCompiler { na::zoned::RoutingAgnosticCompiler::Config config; @@ -72,8 +72,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { .useWindow = useWindow, .windowSize = windowSize, .dynamicPlacement = dynamicPlacement}; - config.codeGeneratorConfig = {.parkingOffset = parkingOffset, - .warnUnsupportedGates = + config.codeGeneratorConfig = {.warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; }), @@ -86,7 +85,6 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { defaultConfig.layoutSynthesizerConfig.placerConfig.windowSize, "dynamic_placement"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.dynamicPlacement, - "parking_offset"_a = defaultConfig.codeGeneratorConfig.parkingOffset, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } @@ -127,8 +125,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { const double windowRatio, const double windowShare, const float deepeningFactor, const float deepeningValue, const float lookaheadFactor, const float reuseLevel, - const size_t maxNodes, const size_t parkingOffset, - const bool warnUnsupportedGates) + const size_t maxNodes, const bool warnUnsupportedGates) -> na::zoned::RoutingAwareCompiler { na::zoned::RoutingAwareCompiler::Config config; config.logLevel = spdlog::level::from_str(logLevel); @@ -143,8 +140,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { .lookaheadFactor = lookaheadFactor, .reuseLevel = reuseLevel, .maxNodes = maxNodes}; - config.codeGeneratorConfig = {.parkingOffset = parkingOffset, - .warnUnsupportedGates = + config.codeGeneratorConfig = {.warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; }), @@ -169,7 +165,6 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { defaultConfig.layoutSynthesizerConfig.placerConfig.reuseLevel, "max_nodes"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.maxNodes, - "parking_offset"_a = defaultConfig.codeGeneratorConfig.parkingOffset, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } diff --git a/include/na/zoned/code_generator/CodeGenerator.hpp b/include/na/zoned/code_generator/CodeGenerator.hpp index 98ee4cedd..7ee6750b8 100644 --- a/include/na/zoned/code_generator/CodeGenerator.hpp +++ b/include/na/zoned/code_generator/CodeGenerator.hpp @@ -31,15 +31,12 @@ class CodeGenerator { public: /// The configuration of the CodeGenerator struct Config { - /// The offset for parking spots - size_t parkingOffset = 1; /** * Warn if a gate not belonging to the basis gates (local rz, global ry) is * used */ bool warnUnsupportedGates = true; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, parkingOffset, - warnUnsupportedGates); + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, warnUnsupportedGates); }; private: @@ -95,5 +92,106 @@ class CodeGenerator { const Placement& targetPlacement, const std::vector>& atoms, NAComputation& code) const -> void; + + /** + * @brief An auxiliary class to create one rearrangement step. + * @details One rearrangement step comprises all shuttling operations from + * the first load operation to the last store operation before the next load + * operation or unitary operation (gate). + */ + class RearrangementGenerator { + /// The architecture of the neutral atom system + std::reference_wrapper architecture_; + struct QubitMovement { + enum class SiteKind { + STORAGE, + ENTANGLEMENT_LEFT, + ENTANGLEMENT_RIGHT, + }; + SiteKind sourceSite; + int64_t sourceX; + int64_t sourceY; + SiteKind targetSite; + int64_t targetX; + int64_t targetY; + }; + std::unordered_map movements_; + int64_t sourceDx_ = 0; + int64_t sourceDy_ = 0; + int64_t sourceMinX_ = 0; + int64_t sourceMinY_ = 0; + int64_t sourceMaxX_ = 0; + int64_t sourceMaxY_ = 0; + int64_t targetDx_ = 0; + int64_t targetDy_ = 0; + int64_t targetMinX_ = 0; + int64_t targetMinY_ = 0; + int64_t targetMaxX_ = 0; + int64_t targetMaxY_ = 0; + int64_t pairSep_ = 0; + // Since rows cannot split, this map collects the start (key) and end + // (value) y-position of each row that must be moved. It is intentionally an + // 'ordered' map to save the sorting afterward. + std::map verticalMoves_; + // Since columns cannot split, this map collects the start (key) and end + // (value) x-position of each column that must be moved. It is intentionally + // an 'ordered' map to save the sorting afterward. + std::map horizontalMoves_; + bool identicalRowOrder_ = false; + bool identicalColumnOrder_ = false; + enum class RearrangementDirection { + UP, + DOWN, + }; + RearrangementDirection rearrangementDirection_ = RearrangementDirection::UP; + // A map from activated AOD columns to their current x-coordinate. This is + // intentionally an 'ordered' map to ease the pushing of activated columns. + std::map aodColsToX_; + // A map from activated AOD rows to their current y-coordinate. This is + // intentionally an 'ordered' map to ease the pushing of activated rows. + std::map aodRowsToY_; + // A map of shuttling qubits to their current location. This is required to + // compare their latest position with their new position to check whether + // they moved and need to be included in a move operation. This map is + // intentionally an 'ordered' map to ensure deterministic (ordered atoms) + // code generation. + std::map> + shuttlingQubitToCurrentLocation_; + + [[nodiscard]] auto getLocationFromSite(const Site& site) + -> std::pair; + [[nodiscard]] static auto getSiteKindFromSite(const Site& site) + -> QubitMovement::SiteKind; + auto + addSourceMove(const std::unordered_map& sourceXToAodCol, + const std::unordered_map& sourceYToAodRow, + const std::vector>& atoms, + NAComputation& code) -> void; + auto + addTargetMove(const std::unordered_map& targetXToAodCol, + const std::unordered_map& targetYToAodRow, + const std::vector>& atoms, + NAComputation& code) -> void; + auto + loadRowByRow(const std::vector>& atoms, + NAComputation& code) -> void; + auto loadColumnByColumn( + const std::vector>& atoms, + NAComputation& code) -> void; + auto + storeRowByRow(const std::vector>& atoms, + NAComputation& code) -> void; + auto storeColumnByColumn( + const std::vector>& atoms, + NAComputation& code) -> void; + + public: + RearrangementGenerator(const Architecture& arch, + const Placement& sourcePlacement, + const Placement& targetPlacement, + const std::vector& qubits); + auto generate(const std::vector>& atoms, + NAComputation& code) -> void; + }; }; } // namespace na::zoned diff --git a/src/na/zoned/code_generator/CodeGenerator.cpp b/src/na/zoned/code_generator/CodeGenerator.cpp index dc41af6b6..796cf84a2 100644 --- a/src/na/zoned/code_generator/CodeGenerator.cpp +++ b/src/na/zoned/code_generator/CodeGenerator.cpp @@ -40,6 +40,18 @@ #include namespace na::zoned { +// Unique-y comparison (ignores multiplicity) +template + requires std::is_same_v, std::decay_t> +bool isSameMappedSet(Set1&& a, Set2&& b, Map m) { + using T = std::decay_t::value_type>; + std::unordered_set mappedA, mappedB; + std::ranges::for_each( + a, [&m, &mappedA](const auto& i) { mappedA.emplace(m(i)); }); + std::ranges::for_each( + b, [&m, &mappedB](const auto& i) { mappedB.emplace(m(i)); }); + return mappedA == mappedB; +} auto CodeGenerator::appendSingleQubitGates( const size_t nQubits, const SingleQubitGateLayer& singleQubitGates, const std::vector>& atoms, @@ -178,78 +190,943 @@ auto CodeGenerator::appendTwoQubitGates( appendRearrangement(executionPlacement, targetRouting, targetPlacement, atoms, code); } +namespace { +[[nodiscard]] auto enumerate(const auto& data) { + return data | std::views::transform([i = 0UL](const auto& value) mutable { + return std::pair{i++, value}; + }); +} +} // namespace auto CodeGenerator::appendRearrangement( const Placement& startPlacement, const Routing& routing, const Placement& targetPlacement, const std::vector>& atoms, NAComputation& code) const -> void { for (const auto& qubits : routing) { - std::map> rowsWithQubits; - std::vector atomsToMove; - std::vector targetLocations; - for (const auto& qubit : qubits) { - // get the current location of the qubit - const auto& [slm, r, c] = startPlacement[qubit]; - const auto& [x, y] = architecture_.get().exactSLMLocation(slm, r, c); - rowsWithQubits.try_emplace(y).first->second.emplace(x, qubit); - atomsToMove.emplace_back(&atoms[qubit].get()); - // get the target location of the qubit - const auto& [targetSLM, targetR, targetC] = targetPlacement[qubit]; - const auto& [targetX, targetY] = - architecture_.get().exactSLMLocation(targetSLM, targetR, targetC); - targetLocations.emplace_back( - Location{static_cast(targetX), static_cast(targetY)}); - } - std::vector>> - alreadyLoadedQubits; - const auto& [minY, firstRow] = *rowsWithQubits.cbegin(); - std::vector firstAtomsToLoad; - firstAtomsToLoad.reserve(firstRow.size()); - for (const auto& [x, qubit] : firstRow) { - alreadyLoadedQubits.emplace_back(qubit, std::pair{x, minY}); - firstAtomsToLoad.emplace_back(&atoms[qubit].get()); - } - code.emplaceBack(firstAtomsToLoad); - // if there are more than one row with atoms to move, we pick them up - // row-by-row as a simple strategy to avoid ghost-spots - for (auto it = std::next(rowsWithQubits.cbegin()); - it != rowsWithQubits.cend(); ++it) { - const auto& [yCoordinateOfRow, row] = *it; - // perform an offset move to avoid ghost-spots - std::vector atomsToOffset; - std::vector offsetTargetLocations; - atomsToOffset.reserve(alreadyLoadedQubits.size()); - offsetTargetLocations.reserve(alreadyLoadedQubits.size()); - for (const auto& [qubit, location] : alreadyLoadedQubits) { - atomsToOffset.emplace_back(&atoms[qubit].get()); - const auto& [x, y] = location; - if (row.find(x) != row.end()) { - // new atoms get picked up in the column at x, i.e., only do a - // vertical offset - offsetTargetLocations.emplace_back( - Location{static_cast(x), - static_cast(y + config_.parkingOffset)}); + RearrangementGenerator rearrangementGenerator( + architecture_.get(), startPlacement, targetPlacement, qubits); + rearrangementGenerator.generate(atoms, code); + } +} +auto CodeGenerator::RearrangementGenerator::getLocationFromSite( + const Site& site) -> std::pair { + const auto& [slm, r, c] = site; + const auto& [x, y] = architecture_.get().exactSLMLocation(slm, r, c); + return {static_cast(x), static_cast(y)}; +} +auto CodeGenerator::RearrangementGenerator::getSiteKindFromSite( + const Site& site) -> QubitMovement::SiteKind { + const auto& slm = std::get<0>(site).get(); + if (slm.isStorage()) { + return QubitMovement::SiteKind::STORAGE; + } + if (slm.entanglementZone_->front().location.first < + slm.entanglementZone_->back().location.first) { + if (slm == slm.entanglementZone_->front()) { + return QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + } + return QubitMovement::SiteKind::ENTANGLEMENT_RIGHT; + } + if (slm == slm.entanglementZone_->back()) { + return QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + } + return QubitMovement::SiteKind::ENTANGLEMENT_RIGHT; +} +auto CodeGenerator::RearrangementGenerator::addSourceMove( + const std::unordered_map& sourceXToAodCol, + const std::unordered_map& sourceYToAodRow, + const std::vector>& atoms, + NAComputation& code) -> void { + std::vector atomsToOffset; + std::vector offsetTargetLocations; + for (auto& [qubit, location] : shuttlingQubitToCurrentLocation_) { + const auto& qubitMovement = movements_.at(qubit); + const std::pair newLocation{ + aodColsToX_.at(sourceXToAodCol.at(qubitMovement.sourceX)), + aodRowsToY_.at(sourceYToAodRow.at(qubitMovement.sourceY))}; + if (location != newLocation) { + atomsToOffset.emplace_back(&atoms[qubit].get()); + offsetTargetLocations.emplace_back( + static_cast(newLocation.first), + static_cast(newLocation.second)); + location = newLocation; + } + } + if (!atomsToOffset.empty()) { + code.emplaceBack(atomsToOffset, offsetTargetLocations); + } +} +auto CodeGenerator::RearrangementGenerator::addTargetMove( + const std::unordered_map& targetXToAodCol, + const std::unordered_map& targetYToAodRow, + const std::vector>& atoms, + NAComputation& code) -> void { + std::vector atomsToOffset; + std::vector offsetTargetLocations; + for (auto& [qubit, location] : shuttlingQubitToCurrentLocation_) { + const auto& qubitMovement = movements_.at(qubit); + const std::pair newLocation{ + aodColsToX_.at(targetXToAodCol.at(qubitMovement.targetX)), + aodRowsToY_.at(targetYToAodRow.at(qubitMovement.targetY))}; + if (location != newLocation) { + atomsToOffset.emplace_back(&atoms[qubit].get()); + offsetTargetLocations.emplace_back( + static_cast(newLocation.first), + static_cast(newLocation.second)); + location = newLocation; + } + } + if (!atomsToOffset.empty()) { + code.emplaceBack(atomsToOffset, offsetTargetLocations); + } +} +auto CodeGenerator::RearrangementGenerator::loadRowByRow( + const std::vector>& atoms, + NAComputation& code) -> void { + // Map collecting all atoms that must be loaded within each source row + // (y-coordinate). It is intentionally an 'ordered' map to save the sorting + // afterward. This set is intentionally an 'ordered' map to ensure + // deterministic (ordered atoms) code generation. + std::map> yToQubitsToBeLoaded; + + for (const auto& [qubit, movement] : movements_) { + // record the qubits in each row to be loaded + yToQubitsToBeLoaded.try_emplace(movement.sourceY) + .first->second.emplace(qubit); + } + + // Since rows cannot split, this map collects the end (key) and start + // (value) y-position of each row that must be moved. It is intentionally an + // 'ordered' map to save the sorting afterward. + std::map revVerticalMoves; + for (const auto& [k, v] : verticalMoves_) { + revVerticalMoves.emplace(v, k); + } + + // A map from the source y-coordinate of the row to the AOD row that will + // load the atoms in this row. Here it is important that the moves are + // sorted by their final y-coordinate. + std::unordered_map sourceYToAodRow; + for (const auto& [aodRow, revMove] : enumerate(revVerticalMoves)) { + sourceYToAodRow.emplace(revMove.second, aodRow); + } + // A map from the source x-coordinate of the column to the AOD column that + // will load the atoms in this column. + std::unordered_map sourceXToAodCol; + for (const auto& [aodCol, x] : + enumerate(horizontalMoves_ | std::views::keys)) { + sourceXToAodCol.emplace(x, aodCol); + } + + // Load the atoms row-wise + const int64_t sign = + (rearrangementDirection_ == RearrangementDirection::UP ? 1 : -1); + for (const auto& [sourceY, qubitsToLoad] : yToQubitsToBeLoaded) { + // Get the AOD row to load the atoms in this row. + assert(sourceYToAodRow.contains(sourceY)); + const auto newAodRow = sourceYToAodRow[sourceY]; + if (aodRowsToY_.contains(newAodRow)) { + // Atoms in this column already loaded, skip + continue; + } + auto allQubitsToLoad = qubitsToLoad; + // already include a virtual offset move by `startD / 2` + const auto y = sourceY + (sign * sourceDy_ / 2); + const auto it = aodRowsToY_.emplace(newAodRow, y).first; + // Push already activated rows away if necessary. + auto nextY = y - sourceDy_; + for (auto lowerIt = std::make_reverse_iterator(it); + lowerIt != aodRowsToY_.rend() && lowerIt->second > nextY; ++lowerIt) { + lowerIt->second = nextY; + nextY -= nextY > sourceMinY_ ? sourceDy_ : sourceDy_ / 2; + } + nextY = y + sourceDy_; + for (auto upperIt = std::next(it); true; ++upperIt) { + // check whether another aod row could be loaded + for (auto anotherSourceYIt = + yToQubitsToBeLoaded.lower_bound(nextY - (sign * sourceDy_ / 2)); + anotherSourceYIt != yToQubitsToBeLoaded.end(); ++anotherSourceYIt) { + if (const auto anotherAodRow = sourceYToAodRow[anotherSourceYIt->first]; + std::prev(upperIt)->first < anotherAodRow) { + if (upperIt == aodRowsToY_.end() || anotherAodRow < upperIt->first) { + if (isSameMappedSet(qubitsToLoad, anotherSourceYIt->second, + [this](const auto q) { + return movements_.at(q).sourceX; + })) { + aodRowsToY_.emplace(sourceYToAodRow[anotherSourceYIt->first], + anotherSourceYIt->first + + (sign * sourceDy_ / 2)); + std::ranges::for_each(anotherSourceYIt->second, + [&allQubitsToLoad](const auto q) { + allQubitsToLoad.emplace(q); + }); + nextY = + anotherSourceYIt->first + sourceDy_ + (sign * sourceDy_ / 2); + } + } + } + } + if (upperIt == aodRowsToY_.end()) { + break; + } + if (upperIt->second < nextY) { + upperIt->second = nextY; + nextY += nextY < sourceMaxY_ ? sourceDy_ : sourceDy_ / 2; + } + } + addSourceMove(sourceXToAodCol, sourceYToAodRow, atoms, code); + // Align aod columns + for (const auto qubit : qubitsToLoad) { + const auto& qubitMovement = movements_.at(qubit); + const auto aodCol = sourceXToAodCol.at(qubitMovement.sourceX); + aodColsToX_[aodCol] = qubitMovement.sourceX; + } + addSourceMove(sourceXToAodCol, sourceYToAodRow, atoms, code); + // Load new atoms + std::vector atomsToLoad; + for (const auto& qubit : allQubitsToLoad) { + atomsToLoad.emplace_back(&atoms[qubit].get()); + const auto& qubitMovement = movements_.at(qubit); + shuttlingQubitToCurrentLocation_.emplace( + qubit, std::pair{qubitMovement.sourceX, qubitMovement.sourceY}); + // Make a virtual offset of columns with new atoms + const auto aodCol = sourceXToAodCol.at(qubitMovement.sourceX); + if (qubitMovement.sourceSite == QubitMovement::SiteKind::STORAGE) { + aodColsToX_[aodCol] = qubitMovement.sourceX + sourceDx_ / 2; + } else { + if (qubitMovement.sourceSite == + QubitMovement::SiteKind::ENTANGLEMENT_LEFT) { + aodColsToX_[aodCol] = qubitMovement.sourceX - sourceDx_ / 4; + } else { + aodColsToX_[aodCol] = qubitMovement.sourceX + sourceDx_ / 4; + } + } + } + code.emplaceBack(atomsToLoad); + addSourceMove(sourceXToAodCol, sourceYToAodRow, atoms, code); + } +} +auto CodeGenerator::RearrangementGenerator::loadColumnByColumn( + const std::vector>& atoms, + NAComputation& code) -> void { + // Map collecting all atoms that must be loaded within each source column + // (x-coordinate). It is intentionally an 'ordered' map to save the sorting + // afterward. This set is intentionally an 'ordered' map to ensure + // deterministic (ordered atoms) code generation. + std::map> xToQubitsToBeLoaded; + + for (const auto& [qubit, movement] : movements_) { + // record the qubits in each column to be loaded + xToQubitsToBeLoaded.try_emplace(movement.sourceX) + .first->second.emplace(qubit); + } + + // A map from the source x-coordinate of the column to the AOD column that + // will load the atoms in this column. Since loadColumnByColumn is always + // followed by storeColumnByColumn, see `generate(...)`, we do not attempt + // reordering columns while loading. Hence, we enumerate and sort the columns + // by their initial x-coordinate + std::unordered_map sourceXToAodCol; + for (const auto& [aodCol, revMove] : enumerate(horizontalMoves_)) { + sourceXToAodCol.emplace(revMove.first, aodCol); + } + // A map from the source y-coordinate of the row to the AOD row that + // will load the atoms in this column. + std::unordered_map sourceYToAodRow; + for (const auto& [aodRow, y] : enumerate(verticalMoves_ | std::views::keys)) { + sourceYToAodRow.emplace(y, aodRow); + } + + const int64_t sign = + (rearrangementDirection_ == RearrangementDirection::UP ? 1 : -1); + // Load the atoms column-wise + for (const auto& [sourceX, qubitsToLoad] : xToQubitsToBeLoaded) { + // Get the AOD column to load the atoms in this column. + assert(sourceXToAodCol.contains(sourceX)); + const auto newAodCol = sourceXToAodCol[sourceX]; + if (aodColsToX_.contains(newAodCol)) { + // Atoms in this column already loaded, skip + continue; + } + auto allQubitsToLoad = qubitsToLoad; + if (const auto columnKind = movements_.at(*qubitsToLoad.begin()).sourceSite; + columnKind == QubitMovement::SiteKind::STORAGE) { + // already include a virtual offset move by `startDx / 2` + const auto it = + aodColsToX_.emplace(newAodCol, sourceX + sourceDx_ / 2).first; + // Push already activated columns away if necessary. + auto nextX = sourceX - (sourceDx_ / 2); + for (auto lowerIt = std::make_reverse_iterator(it); + lowerIt != aodColsToX_.rend() && lowerIt->second > nextX; + ++lowerIt) { + lowerIt->second = nextX; + nextX -= nextX > sourceMinX_ ? sourceDx_ : sourceDx_ / 2; + } + nextX = sourceX + sourceDx_ + (sourceDx_ / 2); + for (auto upperIt = std::next(it); true; ++upperIt) { + // check whether another aod column could be loaded + for (auto anotherSourceXIt = + xToQubitsToBeLoaded.lower_bound(nextX - sourceDx_ / 2); + anotherSourceXIt != xToQubitsToBeLoaded.end(); + ++anotherSourceXIt) { + if (const auto anotherAodCol = + sourceXToAodCol[anotherSourceXIt->first]; + std::prev(upperIt)->first < anotherAodCol) { + if (upperIt == aodColsToX_.end() || + anotherAodCol < upperIt->first) { + if (isSameMappedSet(qubitsToLoad, anotherSourceXIt->second, + [this](const auto q) { + return movements_.at(q).sourceY; + })) { + aodColsToX_.emplace(anotherAodCol, + anotherSourceXIt->first + sourceDx_ / 2); + std::ranges::for_each(anotherSourceXIt->second, + [&allQubitsToLoad](const auto q) { + allQubitsToLoad.emplace(q); + }); + nextX = anotherSourceXIt->first + sourceDx_ + (sourceDx_ / 2); + } + } + } + } + if (upperIt == aodColsToX_.end()) { + break; + } + if (upperIt->second < nextX) { + upperIt->second = nextX; + nextX += nextX < sourceMaxX_ ? sourceDx_ : sourceDx_ / 2; + } + } + } else { + bool left = columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + const auto offsetX = (sourceDx_ - pairSep_) / 3; + // already include a virtual offset move + const auto it = + aodColsToX_.emplace(newAodCol, sourceX + (left ? -offsetX : offsetX)) + .first; + // Push already activated columns away if necessary. + auto nextX = sourceX - + (left ? sourceDx_ - pairSep_ - offsetX : pairSep_ + offsetX); + for (auto lowerIt = std::make_reverse_iterator(it); + lowerIt != aodColsToX_.rend() && lowerIt->second > nextX; + ++lowerIt) { + lowerIt->second = nextX; + nextX -= + ((nextX > sourceMinX_ && left) ? sourceDx_ - offsetX : offsetX); + left = !left; + } + left = columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + nextX = sourceX + + (left ? pairSep_ + offsetX : sourceDx_ - pairSep_ - offsetX); + for (auto upperIt = std::next(it); true; ++upperIt) { + // check whether another aod column could be loaded + for (auto anotherSourceXIt = xToQubitsToBeLoaded.lower_bound( + nextX - + (left ? pairSep_ + offsetX : sourceDx_ - pairSep_ - offsetX)); + anotherSourceXIt != xToQubitsToBeLoaded.end(); + ++anotherSourceXIt) { + if (const auto anotherAodCol = + sourceXToAodCol[anotherSourceXIt->first]; + std::prev(upperIt)->first < anotherAodCol) { + if (upperIt == aodColsToX_.end() || + anotherAodCol < upperIt->first) { + if (isSameMappedSet(qubitsToLoad, anotherSourceXIt->second, + [this](const auto q) { + return movements_.at(q).sourceY; + })) { + left = + movements_[*anotherSourceXIt->second.cbegin()].sourceSite == + QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + aodColsToX_.emplace(anotherAodCol, + anotherSourceXIt->first + + (left ? -offsetX : offsetX)); + std::ranges::for_each(anotherSourceXIt->second, + [&allQubitsToLoad](const auto q) { + allQubitsToLoad.emplace(q); + }); + nextX = anotherSourceXIt->first + + (left ? pairSep_ + offsetX + : sourceDx_ - pairSep_ - offsetX); + } + } + } + } + if (upperIt == aodColsToX_.end()) { + break; + } + if (upperIt->second < nextX) { + upperIt->second = nextX; + nextX += nextX >= sourceMaxX_ || left ? offsetX : sourceDx_ - offsetX; + left = !left; + } + } + } + addSourceMove(sourceXToAodCol, sourceYToAodRow, atoms, code); + // Align aod rows + for (const auto qubit : qubitsToLoad) { + const auto& qubitMovement = movements_.at(qubit); + const auto aodRow = sourceYToAodRow.at(qubitMovement.sourceY); + aodRowsToY_[aodRow] = qubitMovement.sourceY; + } + // Write out offset move before loading new atoms + addSourceMove(sourceXToAodCol, sourceYToAodRow, atoms, code); + // Load new atoms + std::vector atomsToLoad; + for (const auto& qubit : allQubitsToLoad) { + atomsToLoad.emplace_back(&atoms[qubit].get()); + const auto& qubitMovement = movements_.at(qubit); + shuttlingQubitToCurrentLocation_.emplace( + qubit, std::pair{qubitMovement.sourceX, qubitMovement.sourceY}); + // Make a virtual offset of rows with new atoms + const auto aodRow = sourceYToAodRow.at(qubitMovement.sourceY); + aodRowsToY_[aodRow] = qubitMovement.sourceY + (sign * sourceDy_ / 2); + } + code.emplaceBack(atomsToLoad); + addSourceMove(sourceXToAodCol, sourceYToAodRow, atoms, code); + } +} +auto CodeGenerator::RearrangementGenerator::storeRowByRow( + const std::vector>& atoms, + NAComputation& code) -> void { + const int64_t sign = + (rearrangementDirection_ == RearrangementDirection::DOWN ? 1 : -1); + // Since storeRowByRow is only called after loadRowByRow, the rows are already + // in order. Hence, the AOD rows must be enumerated and sorted by their final + // y-coordinate. See also `generate(...)`. + std::map revVerticalMoves; + for (const auto& [k, v] : verticalMoves_) { + revVerticalMoves.emplace(v, k); + } + // A map from the target y-coordinate of the row to the AOD row that + // will store the atoms in this row. + std::unordered_map targetYToAodRow; + std::unordered_map aodRowToTargetY; + for (const auto& [aodRow, move] : enumerate(revVerticalMoves)) { + targetYToAodRow.emplace(move.first, aodRow); + aodRowToTargetY.emplace(aodRow, move.first); + aodRowsToY_[aodRow] = move.first + (sign * targetDy_ / 2); + } + // A set of target x-coordinate of the columns to the AOD columns that + // will store the atoms in this column + std::set targetXs; + for (const auto x : horizontalMoves_ | std::views::values) { + targetXs.emplace(x); + } + std::unordered_map targetXToAodCol; + for (const auto& [aodCol, x] : enumerate(targetXs)) { + targetXToAodCol.emplace(x, aodCol); + } + + // Map collecting all atoms that must be stored within each target row + // (y-coordinate). It is intentionally an 'ordered' map to save the sorting + // afterward. This set is intentionally an 'ordered' map to ensure + // deterministic (ordered atoms) code generation. + std::map> yToQubitsToBeStored; + + for (const auto& [qubit, movement] : movements_) { + // record the qubits in each column to be loaded + yToQubitsToBeStored.try_emplace(movement.targetY) + .first->second.emplace(qubit); + // Make a virtual move of all columns to their target x-coordinates + const auto x = movement.targetX; + const auto aodCol = targetXToAodCol.at(x); + if (movement.targetSite == QubitMovement::SiteKind::STORAGE) { + aodColsToX_[aodCol] = x + targetDx_ / 2; + } else { + if (movement.targetSite == QubitMovement::SiteKind::ENTANGLEMENT_LEFT) { + aodColsToX_[aodCol] = x - targetDx_ / 4; + } else { + aodColsToX_[aodCol] = x + targetDx_ / 4; + } + } + } + + { + const auto firstTargetY = yToQubitsToBeStored.cbegin()->first; + assert(targetYToAodRow.contains(firstTargetY)); + const auto oldAodRow = targetYToAodRow[firstTargetY]; + const auto it = aodRowsToY_.find(oldAodRow); + const auto y = it->second; + auto nextY = y; + for (auto lowerIt = std::make_reverse_iterator(it); + lowerIt != aodRowsToY_.rend(); ++lowerIt) { + nextY -= nextY > targetMinY_ ? targetDy_ : targetDy_ / 2; + if (lowerIt->second > nextY) { + lowerIt->second = nextY; + } else { + nextY = lowerIt->second; + } + } + nextY = y; + for (auto upperIt = std::next(it); upperIt != aodRowsToY_.end(); + ++upperIt) { + nextY += nextY < targetMaxY_ ? targetDy_ : targetDy_ / 2; + if (upperIt->second < nextY) { + upperIt->second = nextY; + } else { + nextY = upperIt->second; + } + } + } + + for (const auto& [targetY, qubitsToStore] : yToQubitsToBeStored) { + // Get the AOD column to store the atom from + assert(targetYToAodRow.contains(targetY)); + const auto oldAodRow = targetYToAodRow[targetY]; + const auto it = aodRowsToY_.find(oldAodRow); + if (it == aodRowsToY_.end()) { + // Atoms in this row already stored, skip + continue; + } + auto allQubitsToStore = qubitsToStore; + std::vector rowsToStore{std::pair{oldAodRow, targetY}}; + const auto y = targetY + (sign * targetDy_ / 2); + it->second = y; + // Push still activated columns away if necessary + auto nextY = y - targetDy_; + for (auto lowerIt = std::make_reverse_iterator(it); + lowerIt != aodRowsToY_.rend() && lowerIt->second > nextY; ++lowerIt) { + lowerIt->second = nextY; + nextY -= nextY > targetMinY_ ? targetDy_ : targetDy_ / 2; + } + nextY = y + targetDy_; + // counts the activated columns after the last column that was aligned + int64_t freeCols = 0; + for (auto upperIt = std::next(it); upperIt != aodRowsToY_.end(); + ++upperIt) { + // check whether another aod row could also be stored + const auto aodRowTargetY = aodRowToTargetY[upperIt->first]; + const auto& aodColQubits = yToQubitsToBeStored[aodRowTargetY]; + if (aodRowTargetY >= + aodRowTargetY - (sign * targetDy_ / 2) + (freeCols * targetDy_) && + isSameMappedSet(qubitsToStore, aodColQubits, [this](const auto q) { + return movements_.at(q).targetX; + })) { + rowsToStore.emplace_back(upperIt->first, aodRowTargetY); + aodRowsToY_[upperIt->first] = aodRowTargetY + (sign * targetDy_ / 2); + std::ranges::for_each(aodColQubits, [&allQubitsToStore](const auto q) { + allQubitsToStore.emplace(q); + }); + // Push free columns away if necessary + nextY = aodRowTargetY - (sign * targetDy_ / 2); + for (auto lowerIt = std::make_reverse_iterator(upperIt); + lowerIt != aodRowsToY_.rend() && lowerIt->second > nextY; + ++lowerIt) { + lowerIt->second = nextY; + nextY -= nextY > targetMinY_ ? targetDy_ : targetDy_ / 2; + } + nextY = aodRowTargetY + targetDy_ + (sign * targetDy_ / 2); + freeCols = 0; + } else if (upperIt->second < nextY) { + upperIt->second = nextY; + nextY += nextY < targetMaxY_ ? targetDy_ : targetDy_ / 2; + } else { + ++freeCols; + } + } + addTargetMove(targetXToAodCol, targetYToAodRow, atoms, code); + // Align aod columns + for (const auto qubit : qubitsToStore) { + const auto& qubitMovement = movements_.at(qubit); + const auto aodCol = targetXToAodCol.at(qubitMovement.targetX); + aodColsToX_[aodCol] = qubitMovement.targetX; + } + for (const auto [row, rowTargetY] : rowsToStore) { + aodRowsToY_[row] = rowTargetY; + } + addTargetMove(targetXToAodCol, targetYToAodRow, atoms, code); + // Store old atoms + std::vector atomsToStore; + for (const auto& qubit : allQubitsToStore) { + atomsToStore.emplace_back(&atoms[qubit].get()); + shuttlingQubitToCurrentLocation_.erase(qubit); + const auto& qubitMovement = movements_.at(qubit); + const auto aodCol = targetXToAodCol.at(qubitMovement.targetX); + if (qubitMovement.targetSite == QubitMovement::SiteKind::STORAGE) { + aodColsToX_[aodCol] = qubitMovement.targetX + (targetDx_ / 2); + } else if (qubitMovement.targetSite == + QubitMovement::SiteKind::ENTANGLEMENT_LEFT) { + aodColsToX_[aodCol] = qubitMovement.targetX - (targetDx_ / 4); + } else { + aodColsToX_[aodCol] = qubitMovement.targetX + (targetDx_ / 4); + } + } + code.emplaceBack(atomsToStore); + for (const auto row : rowsToStore | std::views::keys) { + aodRowsToY_.erase(row); + } + addTargetMove(targetXToAodCol, targetYToAodRow, atoms, code); + } +} +auto CodeGenerator::RearrangementGenerator::storeColumnByColumn( + const std::vector>& atoms, + NAComputation& code) -> void { + // Map collecting all atoms that must be stored within each target column + // (x-coordinate). It is intentionally an 'ordered' map to save the sorting + // afterward. This set is intentionally an 'ordered' map to ensure + // deterministic (ordered atoms) code generation. + std::map> xToQubitsToBeStored; + + for (const auto& [qubit, movement] : movements_) { + // record the qubits in each column to be loaded + xToQubitsToBeStored.try_emplace(movement.targetX) + .first->second.emplace(qubit); + } + + // A map from the target x-coordinate of the column to the AOD column that + // will store the atoms in this column. Here it is important that the moves + // are sorted by their initial x-coordinate. + std::unordered_map targetXToAodCol; + std::unordered_map aodColToTargetX; + for (const auto& [aodCol, move] : enumerate(horizontalMoves_)) { + targetXToAodCol.emplace(move.second, aodCol); + aodColToTargetX.emplace(aodCol, move.second); + } + // A set of y-coordinates of the rows to the AOD rows that + // will store the atoms in this column + std::set targetYs; + for (const auto v : verticalMoves_ | std::views::values) { + targetYs.emplace(v); + } + std::unordered_map targetYToAodRow; + const int64_t sign = + (rearrangementDirection_ == RearrangementDirection::DOWN ? 1 : -1); + for (const auto& [aodRow, y] : enumerate(targetYs)) { + targetYToAodRow.emplace(y, aodRow); + // Make a virtual move of all rows to their target y-coordinates + aodRowsToY_[aodRow] = y + (sign * targetDy_ / 2); + } + + for (const auto& [targetX, qubitsToStore] : xToQubitsToBeStored) { + // Make a virtual move of all columns to their target x-coordinates + assert(targetXToAodCol.contains(targetX)); + const auto aodCol = targetXToAodCol[targetX]; + if (const auto columnKind = + movements_.at(*qubitsToStore.begin()).targetSite; + columnKind == QubitMovement::SiteKind::STORAGE) { + aodColsToX_[aodCol] = targetX + targetDx_ / 2; + } else { + const auto offsetX = (targetDx_ - pairSep_) / 3; + if (columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT) { + aodColsToX_[aodCol] = targetX - offsetX; + } else { + aodColsToX_[aodCol] = targetX + offsetX; + } + } + } + + // align the columns to the target region, i.e., align the first column to be + // stored and spread the others around + { + const auto& [firstTargetX, firstQubitsToStore] = + *xToQubitsToBeStored.cbegin(); + assert(targetXToAodCol.contains(firstTargetX)); + const auto oldAodCol = targetXToAodCol[firstTargetX]; + const auto it = aodColsToX_.find(oldAodCol); + const auto x = it->second; + auto columnKind = movements_.at(*firstQubitsToStore.begin()).targetSite; + auto nextX = x; + for (auto lowerIt = std::make_reverse_iterator(it); + lowerIt != aodColsToX_.rend(); ++lowerIt) { + if (columnKind == QubitMovement::SiteKind::STORAGE) { + nextX -= nextX > targetMinX_ ? targetDx_ : targetDx_ / 2; + } else { + const auto offsetX = (targetDx_ - pairSep_) / 3; + nextX -= + nextX > targetMinX_ && + columnKind == QubitMovement::SiteKind::ENTANGLEMENT_RIGHT + ? targetDx_ - offsetX + : offsetX; + columnKind = columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT + ? QubitMovement::SiteKind::ENTANGLEMENT_RIGHT + : QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + } + if (lowerIt->second > nextX) { + lowerIt->second = nextX; + } else { + if (columnKind != QubitMovement::SiteKind::STORAGE && + (nextX - lowerIt->second) % targetDx_ != 0) { + columnKind = columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT + ? QubitMovement::SiteKind::ENTANGLEMENT_RIGHT + : QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + } + nextX = lowerIt->second; + } + } + columnKind = movements_.at(*firstQubitsToStore.begin()).targetSite; + nextX = x; + for (auto upperIt = std::next(it); upperIt != aodColsToX_.end(); + ++upperIt) { + if (columnKind == QubitMovement::SiteKind::STORAGE) { + nextX += nextX > targetMinX_ ? targetDx_ : targetDx_ / 2; + } else { + const auto offsetX = (targetDx_ - pairSep_) / 3; + nextX += + nextX > targetMinX_ && + columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT + ? targetDx_ - offsetX + : offsetX; + columnKind = columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT + ? QubitMovement::SiteKind::ENTANGLEMENT_RIGHT + : QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + } + if (upperIt->second < nextX) { + upperIt->second = nextX; + } else { + if (columnKind != QubitMovement::SiteKind::STORAGE && + (upperIt->second - nextX) % targetDx_ != 0) { + columnKind = columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT + ? QubitMovement::SiteKind::ENTANGLEMENT_RIGHT + : QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + } + nextX = upperIt->second; + } + } + } + + for (const auto& [targetX, qubitsToStore] : xToQubitsToBeStored) { + // Get the AOD column to store the atom from + assert(targetXToAodCol.contains(targetX)); + const auto oldAodCol = targetXToAodCol[targetX]; + const auto it = aodColsToX_.find(oldAodCol); + if (it == aodColsToX_.end()) { + // Atoms in this column already stored, skip + continue; + } + auto allQubitsToStore = qubitsToStore; + std::vector colsToStore{std::pair{oldAodCol, targetX}}; + if (const auto columnKind = + movements_.at(*qubitsToStore.begin()).targetSite; + columnKind == QubitMovement::SiteKind::STORAGE) { + it->second = targetX + (targetDx_ / 2); + // Push still activated columns away if necessary + auto nextX = targetX - (targetDx_ / 2); + for (auto lowerIt = std::make_reverse_iterator(it); + lowerIt != aodColsToX_.rend() && lowerIt->second > nextX; + ++lowerIt) { + lowerIt->second = nextX; + nextX -= nextX > targetMinX_ ? targetDx_ : targetDx_ / 2; + } + nextX = targetX + targetDx_ + (targetDx_ / 2); + // counts the activated columns after the last column that was aligned + int64_t freeCols = 0; + for (auto upperIt = std::next(it); upperIt != aodColsToX_.end(); + ++upperIt) { + // check whether this aod column could also be stored + const auto aodColTargetX = aodColToTargetX[upperIt->first]; + const auto& aodColQubits = xToQubitsToBeStored[aodColTargetX]; + if (aodColTargetX >= nextX + (freeCols * targetDx_) - (targetDx_ / 2) && + isSameMappedSet(qubitsToStore, aodColQubits, [this](const auto q) { + return movements_.at(q).targetY; + })) { + colsToStore.emplace_back(upperIt->first, aodColTargetX); + aodColsToX_[upperIt->first] = aodColTargetX + (targetDx_ / 2); + std::ranges::for_each(aodColQubits, + [&allQubitsToStore](const auto q) { + allQubitsToStore.emplace(q); + }); + // Push free columns away if necessary + nextX = aodColTargetX - (targetDx_ / 2); + for (auto lowerIt = std::make_reverse_iterator(upperIt); + lowerIt != aodColsToX_.rend() && lowerIt->second > nextX; + ++lowerIt) { + lowerIt->second = nextX; + nextX -= nextX > targetMinX_ ? targetDx_ : targetDx_ / 2; + } + nextX = aodColTargetX + targetDx_ + (targetDx_ / 2); + freeCols = 0; + } else if (upperIt->second < nextX) { + upperIt->second = nextX; + nextX += nextX < targetMaxX_ ? targetDx_ : targetDx_ / 2; + freeCols = 0; } else { - // no new atoms get picked up in the column at x, i.e., do a - // diagonal offset to avoid any ghost-spots - offsetTargetLocations.emplace_back( - Location{static_cast(x + config_.parkingOffset), - static_cast(y + config_.parkingOffset)}); + ++freeCols; } } - code.emplaceBack(atomsToOffset, offsetTargetLocations); - // load the new atoms - std::vector atomsToLoad; - atomsToLoad.reserve(row.size()); - for (const auto& [x, qubit] : row) { - alreadyLoadedQubits.emplace_back(qubit, std::pair{x, yCoordinateOfRow}); - atomsToLoad.emplace_back(&atoms[qubit].get()); + } else { + bool left = columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + const auto offsetX = (targetDx_ - pairSep_) / 3; + it->second = targetX + (left ? -offsetX : offsetX); + // Push already activated columns away if necessary. + auto nextX = targetX - + (left ? targetDx_ - pairSep_ - offsetX : pairSep_ + offsetX); + for (auto lowerIt = std::make_reverse_iterator(it); + lowerIt != aodColsToX_.rend() && lowerIt->second > nextX; + ++lowerIt) { + lowerIt->second = nextX; + nextX -= + ((nextX > targetMinX_ && left) ? targetDx_ - offsetX : offsetX); + left = !left; } - code.emplaceBack(atomsToLoad); + left = columnKind == QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + nextX = targetX + + (left ? pairSep_ + offsetX : targetDx_ - pairSep_ - offsetX); + // counts the activated columns after the last column that was aligned + int64_t freeCols = 0; + for (auto upperIt = std::next(it); upperIt != aodColsToX_.end(); + ++upperIt) { + // check whether this aod column could also be stored + const auto aodColTargetX = aodColToTargetX[upperIt->first]; + const auto& aodColQubits = xToQubitsToBeStored[aodColTargetX]; + if (aodColTargetX >= nextX - (left ? offsetX : -offsetX) + + ((freeCols / 2) * targetDx_) + + (freeCols % 2 == 1 + ? (left ? offsetX : targetDx_ - offsetX) + : 0) && + isSameMappedSet(qubitsToStore, aodColQubits, [this](const auto q) { + return movements_.at(q).targetY; + })) { + left = movements_[*aodColQubits.cbegin()].targetSite == + QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + colsToStore.emplace_back(upperIt->first, aodColTargetX); + aodColsToX_[upperIt->first] = + aodColTargetX + (left ? -offsetX : offsetX); + std::ranges::for_each(aodColQubits, + [&allQubitsToStore](const auto q) { + allQubitsToStore.emplace(q); + }); + // Push free columns away if necessary + nextX = aodColTargetX - + (left ? targetDx_ - pairSep_ - offsetX : pairSep_ + offsetX); + for (auto lowerIt = std::make_reverse_iterator(upperIt); + lowerIt != aodColsToX_.rend() && lowerIt->second > nextX; + ++lowerIt) { + lowerIt->second = nextX; + nextX -= + nextX > targetMinX_ && left ? targetDx_ - offsetX : offsetX; + left = !left; + } + left = movements_[*aodColQubits.cbegin()].targetSite == + QubitMovement::SiteKind::ENTANGLEMENT_LEFT; + nextX = aodColTargetX + + (left ? pairSep_ + offsetX : targetDx_ - pairSep_ - offsetX); + freeCols = 0; + } else if (upperIt->second < nextX) { + upperIt->second = nextX; + nextX += nextX >= targetMaxX_ || left ? offsetX : targetDx_ - offsetX; + left = !left; + freeCols = 0; + } else { + ++freeCols; + } + } + } + addTargetMove(targetXToAodCol, targetYToAodRow, atoms, code); + // Align aod rows + for (const auto qubit : qubitsToStore) { + const auto& qubitMovement = movements_.at(qubit); + const auto aodRow = targetYToAodRow.at(qubitMovement.targetY); + aodRowsToY_[aodRow] = qubitMovement.targetY; } - // all atoms are loaded, now move them to their target locations - code.emplaceBack(atomsToMove, targetLocations); - code.emplaceBack(atomsToMove); + for (const auto [col, colTargetX] : colsToStore) { + aodColsToX_[col] = colTargetX; + } + addTargetMove(targetXToAodCol, targetYToAodRow, atoms, code); + // Store old atoms + std::vector atomsToStore; + for (const auto& qubit : allQubitsToStore) { + atomsToStore.emplace_back(&atoms[qubit].get()); + shuttlingQubitToCurrentLocation_.erase(qubit); + // Make a virtual offset of rows with old atoms + const auto& qubitMovement = movements_.at(qubit); + const auto aodRow = targetYToAodRow.at(qubitMovement.targetY); + aodRowsToY_[aodRow] = qubitMovement.targetY + (sign * targetDy_ / 2); + } + code.emplaceBack(atomsToStore); + for (const auto col : colsToStore | std::views::keys) { + aodColsToX_.erase(col); + } + addTargetMove(targetXToAodCol, targetYToAodRow, atoms, code); + } +} +CodeGenerator::RearrangementGenerator::RearrangementGenerator( + const Architecture& arch, const Placement& sourcePlacement, + const Placement& targetPlacement, const std::vector& qubits) + : architecture_(arch) { + // extract the movement of every single qubit + std::ranges::for_each(qubits, [&](const auto& qubit) { + const auto [sourceX, sourceY] = getLocationFromSite(sourcePlacement[qubit]); + const auto sourceSite = getSiteKindFromSite(sourcePlacement[qubit]); + const auto [targetX, targetY] = getLocationFromSite(targetPlacement[qubit]); + const auto targetSite = getSiteKindFromSite(targetPlacement[qubit]); + movements_.emplace(qubit, QubitMovement{sourceSite, sourceX, sourceY, + targetSite, targetX, targetY}); + }); + + // We assume that all qubits to be loaded are in the same zone. We extract + // the vertical separation of the zone from the first qubit's zone. + const auto& sourceSlm = std::get<0>(sourcePlacement.at(qubits.front())).get(); + sourceDx_ = static_cast(sourceSlm.siteSeparation.first); + sourceDy_ = static_cast(sourceSlm.siteSeparation.second); + sourceMinX_ = static_cast(sourceSlm.location.first); + sourceMaxX_ = sourceMinX_ + sourceDx_ * static_cast(sourceSlm.nCols); + sourceMinY_ = static_cast(sourceSlm.location.first); + sourceMaxY_ = sourceMinY_ + sourceDy_ * static_cast(sourceSlm.nRows); + // We do the same for the target zone + const auto& targetSlm = std::get<0>(targetPlacement.at(qubits.front())).get(); + targetDx_ = static_cast(targetSlm.siteSeparation.first); + targetDy_ = static_cast(targetSlm.siteSeparation.second); + targetMinX_ = static_cast(targetSlm.location.first); + targetMaxX_ = targetMinX_ + targetDx_ * static_cast(targetSlm.nCols); + targetMinY_ = static_cast(targetSlm.location.second); + targetMaxY_ = targetMinY_ + targetDy_ * static_cast(targetSlm.nRows); + + if (sourceSlm.isEntanglement()) { + pairSep_ = + std::abs(static_cast( + sourceSlm.entanglementZone_->back().location.first) - + static_cast( + sourceSlm.entanglementZone_->front().location.first)); + } else { + assert(targetSlm.isEntanglement()); + pairSep_ = + std::abs(static_cast( + targetSlm.entanglementZone_->back().location.first) - + static_cast( + targetSlm.entanglementZone_->front().location.first)); + } + + for (const auto& [qubit, movement] : movements_) { + // record the moves + const auto verticalIt = + verticalMoves_.try_emplace(movement.sourceY, movement.targetY).first; + // If this does not hold, the input was invalid for this generator. + // More precisely, this conditional `assert` ensures that rows do not + // split. + assert(verticalIt->second == movement.targetY); + const auto& horizontalIt = + horizontalMoves_.try_emplace(movement.sourceX, movement.targetX).first; + // If this does not hold, the input was invalid for this generator. + // More precisely, this conditional `assert` ensures that columns do not + // split. + assert(horizontalIt->second == movement.targetX); + } + + // Check the vertical moves whether all rows remain in the same order + identicalRowOrder_ = + std::ranges::is_sorted(verticalMoves_ | std::views::values); + // Check the horizontal moves whether all columns remain in the same order + identicalColumnOrder_ = + std::ranges::is_sorted(horizontalMoves_ | std::views::values); + + const auto anyVerticalMove = verticalMoves_.begin(); + rearrangementDirection_ = anyVerticalMove->first < anyVerticalMove->second + ? RearrangementDirection::UP + : RearrangementDirection::DOWN; +} +auto CodeGenerator::RearrangementGenerator::generate( + const std::vector>& atoms, + NAComputation& code) -> void { + if (identicalRowOrder_ && horizontalMoves_.size() < verticalMoves_.size()) { + loadColumnByColumn(atoms, code); + storeColumnByColumn(atoms, code); + } else if (identicalColumnOrder_ && + verticalMoves_.size() < horizontalMoves_.size()) { + loadRowByRow(atoms, code); + storeRowByRow(atoms, code); + } else { + loadRowByRow(atoms, code); + storeColumnByColumn(atoms, code); } } auto CodeGenerator::generate( From 77e2fbc7e78e4f1f22efe2d385128d6a08699097 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 10:59:16 +0100 Subject: [PATCH 011/119] =?UTF-8?q?=E2=9C=85=20Adapt=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_asap_scheduler.cpp | 21 +-------------------- test/na/zoned/test_compiler.cpp | 2 -- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/test/na/zoned/test_asap_scheduler.cpp b/test/na/zoned/test_asap_scheduler.cpp index 2c9c2c0ef..f8e3d266c 100644 --- a/test/na/zoned/test_asap_scheduler.cpp +++ b/test/na/zoned/test_asap_scheduler.cpp @@ -45,7 +45,7 @@ constexpr std::string_view architectureJson = R"({ class ASAPSchedulerScheduleTest : public ::testing::Test { protected: Architecture architecture; - ASAPScheduler::Config config{.maxFillingFactor = .8}; + nlohmann::json config; ASAPScheduler scheduler; ASAPSchedulerScheduleTest() : architecture(Architecture::fromJSONString(architectureJson)), @@ -213,23 +213,4 @@ TEST_F(ASAPSchedulerScheduleTest, UnsupportedCXGate) { qc.cx(0, 1); EXPECT_THROW(std::ignore = scheduler.schedule(qc), std::invalid_argument); } -TEST_F(ASAPSchedulerScheduleTest, FullEntanglementZone) { - qc::QuantumComputation qc(26); - for (qc::Qubit i = 0; i < 13; ++i) { - qc.cz(2 * i, 2 * i + 1); - } - const auto& [singleQubitGateLayers, twoQubitGateLayers] = - scheduler.schedule(qc); - EXPECT_THAT(singleQubitGateLayers, ::testing::SizeIs(3)); - EXPECT_THAT(twoQubitGateLayers, ::testing::SizeIs(2)); -} -TEST(ASAPSchedulerConfigTest, InvalidMaxFillingFactor) { - const auto architecture = Architecture::fromJSONString(architectureJson); - constexpr ASAPScheduler::Config config1{.maxFillingFactor = 0.}; - EXPECT_THROW(ASAPScheduler scheduler(architecture, config1), - std::invalid_argument); - constexpr ASAPScheduler::Config config2{.maxFillingFactor = 1.1}; - EXPECT_THROW(ASAPScheduler scheduler(architecture, config2), - std::invalid_argument); -} } // namespace na::zoned diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 651dbc3a3..197bd6716 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -49,14 +49,12 @@ constexpr std::string_view routingAgnosticConfiguration = R"({ } }, "codeGeneratorConfig" : { - "parkingOffset" : 1, "warnUnsupportedGates" : false } })"; constexpr std::string_view routingAwareConfiguration = R"({ "logLevel" : 1, "codeGeneratorConfig" : { - "parkingOffset" : 1, "warnUnsupportedGates" : false }, "layoutSynthesizerConfig" : { From 9a053542c94071b2a8b9b9cc093e828746bf83f2 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:19:04 +0100 Subject: [PATCH 012/119] =?UTF-8?q?=E2=9C=85=20Fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_code_generator.cpp | 190 +++++++++++++++----------- 1 file changed, 110 insertions(+), 80 deletions(-) diff --git a/test/na/zoned/test_code_generator.cpp b/test/na/zoned/test_code_generator.cpp index 731de5af9..dd6dc2b83 100644 --- a/test/na/zoned/test_code_generator.cpp +++ b/test/na/zoned/test_code_generator.cpp @@ -485,33 +485,50 @@ TEST_F(CodeGeneratorGenerateTest, TwoQubitGate) { std::vector>>{{{0U, 1U}}, {{0U, 1U}}}) .toString(), - "atom (0.000, 57.000) atom0\n" - "atom (3.000, 57.000) atom1\n" - "@+ load [\n" - " atom0\n" - " atom1\n" - "]\n" - "@+ move [\n" - " (5.000, 70.000) atom0\n" - " (7.000, 70.000) atom1\n" - "]\n" - "@+ store [\n" - " atom0\n" - " atom1\n" - "]\n" - "@+ cz zone_cz0\n" - "@+ load [\n" - " atom0\n" - " atom1\n" - "]\n" - "@+ move [\n" - " (0.000, 57.000) atom0\n" - " (3.000, 57.000) atom1\n" - "]\n" - "@+ store [\n" - " atom0\n" - " atom1\n" - "]\n"); + R"(atom (0.000, 57.000) atom0 +atom (3.000, 57.000) atom1 +@+ load [ + atom0 + atom1 +] +@+ move [ + (1.000, 58.000) atom0 + (4.000, 58.000) atom1 +] +@+ move [ + (2.000, 65.000) atom0 + (10.000, 65.000) atom1 +] +@+ move [ + (5.000, 70.000) atom0 + (7.000, 70.000) atom1 +] +@+ store [ + atom0 + atom1 +] +@+ cz zone_cz0 +@+ load [ + atom0 + atom1 +] +@+ move [ + (2.000, 65.000) atom0 + (10.000, 65.000) atom1 +] +@+ move [ + (1.000, 58.000) atom0 + (4.000, 58.000) atom1 +] +@+ move [ + (0.000, 57.000) atom0 + (3.000, 57.000) atom1 +] +@+ store [ + atom0 + atom1 +] +)"); } TEST_F(CodeGeneratorGenerateTest, Offset) { // STORAGE ... │ ... │ ... @@ -550,58 +567,71 @@ TEST_F(CodeGeneratorGenerateTest, Offset) { std::vector>>{ {{0U, 1U, 2U, 3U}}, {{0U, 1U, 2U, 3U}}}) .toString(), - "atom (0.000, 54.000) atom0\n" - "atom (0.000, 57.000) atom2\n" - "atom (3.000, 54.000) atom1\n" - "atom (3.000, 57.000) atom3\n" - "@+ load [\n" - " atom0\n" - " atom1\n" - "]\n" - "@+ move [\n" - " (0.000, 55.000) atom0\n" - " (3.000, 55.000) atom1\n" - "]\n" - "@+ load [\n" - " atom2\n" - " atom3\n" - "]\n" - "@+ move [\n" - " (5.000, 70.000) atom0\n" - " (7.000, 70.000) atom1\n" - " (5.000, 80.000) atom2\n" - " (7.000, 80.000) atom3\n" - "]\n" - "@+ store [\n" - " atom0\n" - " atom1\n" - " atom2\n" - " atom3\n" - "]\n" - "@+ cz zone_cz0\n" - "@+ load [\n" - " atom0\n" - " atom1\n" - "]\n" - "@+ move [\n" - " (5.000, 71.000) atom0\n" - " (7.000, 71.000) atom1\n" - "]\n" - "@+ load [\n" - " atom2\n" - " atom3\n" - "]\n" - "@+ move [\n" - " (0.000, 54.000) atom0\n" - " (3.000, 54.000) atom1\n" - " (0.000, 57.000) atom2\n" - " (3.000, 57.000) atom3\n" - "]\n" - "@+ store [\n" - " atom0\n" - " atom1\n" - " atom2\n" - " atom3\n" - "]\n"); + R"(atom (0.000, 54.000) atom0 +atom (0.000, 57.000) atom2 +atom (3.000, 54.000) atom1 +atom (3.000, 57.000) atom3 +@+ load [ + atom0 + atom1 + atom2 + atom3 +] +@+ move [ + (1.000, 55.000) atom0 + (4.000, 55.000) atom1 + (1.000, 58.000) atom2 + (4.000, 58.000) atom3 +] +@+ move [ + (2.000, 65.000) atom0 + (10.000, 65.000) atom1 + (2.000, 75.000) atom2 + (10.000, 75.000) atom3 +] +@+ move [ + (5.000, 70.000) atom0 + (7.000, 70.000) atom1 + (5.000, 80.000) atom2 + (7.000, 80.000) atom3 +] +@+ store [ + atom0 + atom1 + atom2 + atom3 +] +@+ cz zone_cz0 +@+ load [ + atom0 + atom1 + atom2 + atom3 +] +@+ move [ + (2.000, 65.000) atom0 + (10.000, 65.000) atom1 + (2.000, 75.000) atom2 + (10.000, 75.000) atom3 +] +@+ move [ + (1.000, 55.000) atom0 + (4.000, 55.000) atom1 + (1.000, 58.000) atom2 + (4.000, 58.000) atom3 +] +@+ move [ + (0.000, 54.000) atom0 + (3.000, 54.000) atom1 + (0.000, 57.000) atom2 + (3.000, 57.000) atom3 +] +@+ store [ + atom0 + atom1 + atom2 + atom3 +] +)"); } } // namespace na::zoned From 03d2c8ccae7f5b5e5b56b70e0116c71fb4ae4254 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:32:23 +0100 Subject: [PATCH 013/119] =?UTF-8?q?=F0=9F=92=9A=20Fix=20clang-tidy=20warni?= =?UTF-8?q?ngs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/code_generator/CodeGenerator.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/na/zoned/code_generator/CodeGenerator.cpp b/src/na/zoned/code_generator/CodeGenerator.cpp index 796cf84a2..cce617701 100644 --- a/src/na/zoned/code_generator/CodeGenerator.cpp +++ b/src/na/zoned/code_generator/CodeGenerator.cpp @@ -43,7 +43,7 @@ namespace na::zoned { // Unique-y comparison (ignores multiplicity) template requires std::is_same_v, std::decay_t> -bool isSameMappedSet(Set1&& a, Set2&& b, Map m) { +auto isSameMappedSet(Set1&& a, Set2&& b, Map m) -> bool { using T = std::decay_t::value_type>; std::unordered_set mappedA, mappedB; std::ranges::for_each( @@ -79,8 +79,7 @@ auto CodeGenerator::appendSingleQubitGates( assert(false); } } else { - const auto opType = op.get().getType(); - if (opType == qc::RY) { + if (const auto opType = op.get().getType(); opType == qc::RY) { code.emplaceBack(globalZone, op.get().getParameter().front()); } else if (opType == qc::Y) { @@ -104,7 +103,7 @@ auto CodeGenerator::appendSingleQubitGates( assert(op.get().getNqubits() == 1); const qc::Qubit qubit = op.get().getTargets().front(); // By default, all variants of rotational z-gates are supported - if (op.get().getType() == qc::RZ) { + if (op.get().getType() == qc::RZ || op.get().getType() == qc::P) { code.emplaceBack(atoms[qubit], op.get().getParameter().front()); } else if (op.get().getType() == qc::Z) { @@ -117,9 +116,6 @@ auto CodeGenerator::appendSingleQubitGates( code.emplaceBack(atoms[qubit], qc::PI_4); } else if (op.get().getType() == qc::Tdg) { code.emplaceBack(atoms[qubit], -qc::PI_4); - } else if (op.get().getType() == qc::P) { - code.emplaceBack(atoms[qubit], - op.get().getParameter().front()); } else { // in this case, the gate is not any variant of a rotational z-gate. // depending on the settings, a warning is printed. @@ -149,16 +145,14 @@ auto CodeGenerator::appendSingleQubitGates( code.emplaceBack(atoms[qubit], qc::PI, 0, qc::PI); } else if (op.get().getType() == qc::Y) { code.emplaceBack(atoms[qubit], qc::PI, qc::PI_2, qc::PI_2); - } else if (op.get().getType() == qc::V) { - code.emplaceBack(atoms[qubit], -qc::PI_2, -qc::PI_2, - qc::PI_2); } else if (op.get().getType() == qc::Vdg) { code.emplaceBack(atoms[qubit], -qc::PI_2, qc::PI_2, -qc::PI_2); } else if (op.get().getType() == qc::SX) { code.emplaceBack(atoms[qubit], qc::PI_2, -qc::PI_2, qc::PI_2); - } else if (op.get().getType() == qc::SXdg) { + } else if (op.get().getType() == qc::SXdg || + op.get().getType() == qc::V) { code.emplaceBack(atoms[qubit], -qc::PI_2, -qc::PI_2, qc::PI_2); } else { @@ -1135,6 +1129,7 @@ auto CodeGenerator::generate( const std::vector& routing) const -> NAComputation { NAComputation code; std::vector> rydbergZones; + rydbergZones.reserve(architecture_.get().rydbergRangeMinX.size()); for (size_t i = 0; i < architecture_.get().rydbergRangeMinX.size(); ++i) { rydbergZones.emplace_back(code.emplaceBackZone( "zone_cz" + std::to_string(i), From 064eef4a23744bf8ce501aa33542069cfca4401d Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:39:05 +0100 Subject: [PATCH 014/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20compilation=20bug?= =?UTF-8?q?=20on=20old=20mac=20OS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/code_generator/CodeGenerator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/na/zoned/code_generator/CodeGenerator.cpp b/src/na/zoned/code_generator/CodeGenerator.cpp index cce617701..8d23fda0d 100644 --- a/src/na/zoned/code_generator/CodeGenerator.cpp +++ b/src/na/zoned/code_generator/CodeGenerator.cpp @@ -241,8 +241,8 @@ auto CodeGenerator::RearrangementGenerator::addSourceMove( if (location != newLocation) { atomsToOffset.emplace_back(&atoms[qubit].get()); offsetTargetLocations.emplace_back( - static_cast(newLocation.first), - static_cast(newLocation.second)); + Location{.x = static_cast(newLocation.first), + .y = static_cast(newLocation.second)}); location = newLocation; } } From aad203b150c842b6cf2729f3a5a1f5451137dfa3 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:52:39 +0100 Subject: [PATCH 015/119] =?UTF-8?q?=E2=9C=85=20Try=20increase=20test=20cov?= =?UTF-8?q?erage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_code_generator.cpp | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/na/zoned/test_code_generator.cpp b/test/na/zoned/test_code_generator.cpp index dd6dc2b83..84d5e6ffc 100644 --- a/test/na/zoned/test_code_generator.cpp +++ b/test/na/zoned/test_code_generator.cpp @@ -634,4 +634,51 @@ atom (3.000, 57.000) atom3 ] )"); } +TEST_F(CodeGeneratorGenerateTest, ColumnByColumn) { + // STORAGE ... │ ... │ ... + // 17 0 1 o o ... │ o o o o ... │ 0 1 o o ... + // 18 2 3 o o ... │ o o o o ... │ 2 3 o o ... + // 19 4 5 o o ... │ o o o o ... │ 4 5 o o ... + // │ ╲╲ │ ↑ ↑ + // ENTANGLEMENT │ ↓↓ │ ╲╲ + // 0 oo ... │ 01 ... │ oo ... + // 1 oo ... │ 23 ... │ oo ... + // 2 oo ... │ 45 ... │ oo ... + // ... │ ... │ ... + const auto& storage = *architecture.storageZones.front(); + const auto& entanglementLeft = + architecture.entanglementZones.front()->front(); + const auto& entanglementRight = + architecture.entanglementZones.front()->back(); + EXPECT_TRUE( + codeGenerator + .generate( + std::vector< + std::vector>>{{}, + {}}, + std::vector, size_t, size_t>>>{ + {{storage, 17, 0}, + {storage, 17, 1}, + {storage, 18, 0}, + {storage, 18, 1}, + {storage, 19, 0}, + {storage, 19, 1}}, + {{entanglementLeft, 0, 0}, + {entanglementRight, 0, 0}, + {entanglementLeft, 1, 0}, + {entanglementRight, 1, 0}, + {entanglementLeft, 2, 0}, + {entanglementRight, 2, 0}}, + {{storage, 17, 0}, + {storage, 17, 1}, + {storage, 18, 0}, + {storage, 18, 1}, + {storage, 19, 0}, + {storage, 19, 1}}}, + std::vector>>{ + {{0U, 1U, 2U, 3U, 4U, 5U}}, {{0U, 1U, 2U, 3U, 4U, 5U}}}) + .validate() + .first); +} } // namespace na::zoned From 493645596d1e356a2f93a83c4c350c5d206a4ba9 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:26:02 +0100 Subject: [PATCH 016/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/code_generator/CodeGenerator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/na/zoned/code_generator/CodeGenerator.cpp b/src/na/zoned/code_generator/CodeGenerator.cpp index 8d23fda0d..9e4427707 100644 --- a/src/na/zoned/code_generator/CodeGenerator.cpp +++ b/src/na/zoned/code_generator/CodeGenerator.cpp @@ -1054,7 +1054,7 @@ CodeGenerator::RearrangementGenerator::RearrangementGenerator( sourceDy_ = static_cast(sourceSlm.siteSeparation.second); sourceMinX_ = static_cast(sourceSlm.location.first); sourceMaxX_ = sourceMinX_ + sourceDx_ * static_cast(sourceSlm.nCols); - sourceMinY_ = static_cast(sourceSlm.location.first); + sourceMinY_ = static_cast(sourceSlm.location.second); sourceMaxY_ = sourceMinY_ + sourceDy_ * static_cast(sourceSlm.nRows); // We do the same for the target zone const auto& targetSlm = std::get<0>(targetPlacement.at(qubits.front())).get(); From 97c60a54e4454643de38f19c2eac335743caf2b3 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:28:22 +0100 Subject: [PATCH 017/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20missing=20construc?= =?UTF-8?q?tor=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/code_generator/CodeGenerator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/na/zoned/code_generator/CodeGenerator.cpp b/src/na/zoned/code_generator/CodeGenerator.cpp index 9e4427707..c866a5fce 100644 --- a/src/na/zoned/code_generator/CodeGenerator.cpp +++ b/src/na/zoned/code_generator/CodeGenerator.cpp @@ -265,8 +265,8 @@ auto CodeGenerator::RearrangementGenerator::addTargetMove( if (location != newLocation) { atomsToOffset.emplace_back(&atoms[qubit].get()); offsetTargetLocations.emplace_back( - static_cast(newLocation.first), - static_cast(newLocation.second)); + Location{.x = static_cast(newLocation.first), + .y = static_cast(newLocation.second)}); location = newLocation; } } From fc8a2d9bbc33810034e903ae78e191b99d603c91 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 1 Dec 2025 11:00:55 +0100 Subject: [PATCH 018/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20logical=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/code_generator/CodeGenerator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/na/zoned/code_generator/CodeGenerator.cpp b/src/na/zoned/code_generator/CodeGenerator.cpp index c866a5fce..50c76e500 100644 --- a/src/na/zoned/code_generator/CodeGenerator.cpp +++ b/src/na/zoned/code_generator/CodeGenerator.cpp @@ -692,7 +692,7 @@ auto CodeGenerator::RearrangementGenerator::storeRowByRow( const auto aodRowTargetY = aodRowToTargetY[upperIt->first]; const auto& aodColQubits = yToQubitsToBeStored[aodRowTargetY]; if (aodRowTargetY >= - aodRowTargetY - (sign * targetDy_ / 2) + (freeCols * targetDy_) && + nextY - (sign * targetDy_ / 2) + (freeCols * targetDy_) && isSameMappedSet(qubitsToStore, aodColQubits, [this](const auto q) { return movements_.at(q).targetX; })) { From 40c596fa2b386ebd90a9105ba3f89c39898efd75 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:11:13 +0100 Subject: [PATCH 019/119] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20better?= =?UTF-8?q?=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_code_generator.cpp | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/na/zoned/test_code_generator.cpp b/test/na/zoned/test_code_generator.cpp index 84d5e6ffc..433116b6d 100644 --- a/test/na/zoned/test_code_generator.cpp +++ b/test/na/zoned/test_code_generator.cpp @@ -681,4 +681,43 @@ TEST_F(CodeGeneratorGenerateTest, ColumnByColumn) { .validate() .first); } +TEST_F(CodeGeneratorGenerateTest, RelaxedRouting) { + // STORAGE ... │ ... │ ... + // 18 0 1 o o ... │ o o o o ... │ 0 1 o o ... + // 19 2 3 o o ... │ o o o o ... │ 2 3 o o ... + // │ ╲╲ │ ↑ ↑ + // ENTANGLEMENT │ ↓↓ │ ╲╲ + // 0 oo ... │ 32 ... │ oo ... + // 1 oo ... │ 10 ... │ oo ... + // ... │ ... │ ... + const auto& storage = *architecture.storageZones.front(); + const auto& entanglementLeft = + architecture.entanglementZones.front()->front(); + const auto& entanglementRight = + architecture.entanglementZones.front()->back(); + EXPECT_TRUE( + codeGenerator + .generate( + std::vector< + std::vector>>{{}, + {}}, + std::vector, size_t, size_t>>>{ + {{storage, 18, 0}, + {storage, 18, 1}, + {storage, 19, 0}, + {storage, 19, 1}}, + {{entanglementRight, 1, 0}, + {entanglementLeft, 1, 0}, + {entanglementRight, 0, 0}, + {entanglementLeft, 0, 0}}, + {{storage, 18, 0}, + {storage, 18, 1}, + {storage, 19, 0}, + {storage, 19, 1}}}, + std::vector>>{ + {{0U, 1U, 2U, 3U}}, {{0U, 1U, 2U, 3U}}}) + .validate() + .first); +} } // namespace na::zoned From 3e7c7bcad5ba66ac8b1219e1c34c78af1b146ddc Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:25:29 +0100 Subject: [PATCH 020/119] =?UTF-8?q?=F0=9F=8E=A8=20Some=20fixes/improvement?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 2 +- src/na/zoned/code_generator/CodeGenerator.cpp | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 1e1a8a8c0..2588957eb 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -62,7 +62,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { py::init([](const na::zoned::Architecture& arch, const std::string& logLevel, const double maxFillingFactor, const bool useWindow, const size_t windowSize, - const bool dynamicPlacement, const size_t parkingOffset, + const bool dynamicPlacement, const bool warnUnsupportedGates) -> na::zoned::RoutingAgnosticCompiler { na::zoned::RoutingAgnosticCompiler::Config config; diff --git a/src/na/zoned/code_generator/CodeGenerator.cpp b/src/na/zoned/code_generator/CodeGenerator.cpp index 50c76e500..9bdbb2c76 100644 --- a/src/na/zoned/code_generator/CodeGenerator.cpp +++ b/src/na/zoned/code_generator/CodeGenerator.cpp @@ -44,8 +44,9 @@ namespace na::zoned { template requires std::is_same_v, std::decay_t> auto isSameMappedSet(Set1&& a, Set2&& b, Map m) -> bool { - using T = std::decay_t::value_type>; - std::unordered_set mappedA, mappedB; + using Value = typename std::decay_t::value_type; + using MappedT = std::decay_t>; + std::unordered_set mappedA, mappedB; std::ranges::for_each( a, [&m, &mappedA](const auto& i) { mappedA.emplace(m(i)); }); std::ranges::for_each( @@ -1037,6 +1038,9 @@ CodeGenerator::RearrangementGenerator::RearrangementGenerator( const Architecture& arch, const Placement& sourcePlacement, const Placement& targetPlacement, const std::vector& qubits) : architecture_(arch) { + if (qubits.empty()) { + return; + } // extract the movement of every single qubit std::ranges::for_each(qubits, [&](const auto& qubit) { const auto [sourceX, sourceY] = getLocationFromSite(sourcePlacement[qubit]); @@ -1104,6 +1108,7 @@ CodeGenerator::RearrangementGenerator::RearrangementGenerator( std::ranges::is_sorted(horizontalMoves_ | std::views::values); const auto anyVerticalMove = verticalMoves_.begin(); + assert(anyVerticalMove != verticalMoves_.end()); rearrangementDirection_ = anyVerticalMove->first < anyVerticalMove->second ? RearrangementDirection::UP : RearrangementDirection::DOWN; From 1353fc4812a96264d0af420b7ca73034acf5ab50 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:55:53 +0100 Subject: [PATCH 021/119] =?UTF-8?q?=F0=9F=93=9D=20Update=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 443eb8aab..fab293a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ ### Changed +- ✨ Enable code generation for relaxed routing constraints ([#848]) ([**@ystade**]) - ✨ Add `max_filling_factor` to scheduler in Zoned Neutral Atom Compiler ([#847]) ([**@ystade**]) ## [3.4.0] - 2025-10-15 @@ -157,6 +158,8 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ +[#848]: https://github.com/munich-quantum-toolkit/qmap/pull/848 +[#847]: https://github.com/munich-quantum-toolkit/qmap/pull/847 [#804]: https://github.com/munich-quantum-toolkit/qmap/pull/804 [#803]: https://github.com/munich-quantum-toolkit/qmap/pull/803 [#796]: https://github.com/munich-quantum-toolkit/qmap/pull/796 From 4890a6d077024701cd4cb99f0d0f65bb5960d261 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:36:06 +0100 Subject: [PATCH 022/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20enumerate=20for=20?= =?UTF-8?q?rvalues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/code_generator/CodeGenerator.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/na/zoned/code_generator/CodeGenerator.cpp b/src/na/zoned/code_generator/CodeGenerator.cpp index 9bdbb2c76..256d648a7 100644 --- a/src/na/zoned/code_generator/CodeGenerator.cpp +++ b/src/na/zoned/code_generator/CodeGenerator.cpp @@ -186,8 +186,9 @@ auto CodeGenerator::appendTwoQubitGates( code); } namespace { -[[nodiscard]] auto enumerate(const auto& data) { - return data | std::views::transform([i = 0UL](const auto& value) mutable { +[[nodiscard]] auto enumerate(auto&& data) { + return std::views::all(std::forward(data)) | + std::views::transform([i = 0UL](const auto& value) mutable { return std::pair{i++, value}; }); } From a7e891ce2f04807f78ea837c80a9decfa4a5dab8 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:54:08 +0100 Subject: [PATCH 023/119] =?UTF-8?q?=F0=9F=8E=A8=20Add=20Upgrading=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UPGRADING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/UPGRADING.md b/UPGRADING.md index 93a4e7ac2..766757259 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,7 +6,11 @@ This document describes breaking changes and how to upgrade. For a complete list As part of this release, the scheduler of the zoned neutral atom compiler now features a new parameter `max_filling_factor`. It allows limiting the maximum number of parallel entangling gates relative to the maximum capacity of the entangling zone. -NOte, the default is set to `0.9`. +Note, the default is set to `0.9`. + +The code generator of the zoned neutral atom compiler is updated to also handle routings that only satisfy relaxed routing constraints. +In contrast to the strict routing, a relaxed routing can change the relative order of atoms. +The constraint that remains is that atoms previously in one row (column) must remain in the same row (column) after the routing. ## [3.4.0] From 18ef464d03f43cbccd11b312fbb07910b8e56aca Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:21:19 +0100 Subject: [PATCH 024/119] =?UTF-8?q?=F0=9F=8E=A8=20Implement=20relaxed=20ro?= =?UTF-8?q?uter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/na/zoned/Compiler.hpp | 70 +++- .../router/RelaxedIndependentSetRouter.hpp | 126 ++++++ .../router/RelaxedIndependentSetRouter.cpp | 369 ++++++++++++++++++ test/na/zoned/test_compiler.cpp | 23 +- 4 files changed, 568 insertions(+), 20 deletions(-) create mode 100644 include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp create mode 100644 src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp diff --git a/include/na/zoned/Compiler.hpp b/include/na/zoned/Compiler.hpp index 011e1c614..07319e1b4 100644 --- a/include/na/zoned/Compiler.hpp +++ b/include/na/zoned/Compiler.hpp @@ -18,6 +18,7 @@ #include "layout_synthesizer/placer/AStarPlacer.hpp" #include "layout_synthesizer/placer/VertexMatchingPlacer.hpp" #include "layout_synthesizer/router/IndependentSetRouter.hpp" +#include "layout_synthesizer/router/RelaxedIndependentSetRouter.hpp" #include "na/NAComputation.hpp" #include "reuse_analyzer/VertexMatchingReuseAnalyzer.hpp" #include "scheduler/ASAPScheduler.hpp" @@ -159,23 +160,14 @@ class Compiler : protected Scheduler, } #endif // SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG - const auto& schedulingStart = std::chrono::system_clock::now(); // CodeQL was not very happy about the structural binding here, hence I // removed it. + SPDLOG_DEBUG("Scheduling..."); + const auto& schedulingStart = std::chrono::system_clock::now(); const auto& schedule = SELF.schedule(qComp); + const auto& schedulingEnd = std::chrono::system_clock::now(); const auto& singleQubitGateLayers = schedule.first; const auto& twoQubitGateLayers = schedule.second; - const auto& schedulingEnd = std::chrono::system_clock::now(); - const auto& reuseQubits = SELF.analyzeReuse(twoQubitGateLayers); - const auto& reuseAnalysisEnd = std::chrono::system_clock::now(); - const auto& [placement, routing] = LayoutSynthesizer::synthesize( - qComp.getNqubits(), twoQubitGateLayers, reuseQubits); - const auto& layoutSynthesisEnd = std::chrono::system_clock::now(); - NAComputation code = - SELF.generate(singleQubitGateLayers, placement, routing); - const auto& codeGenerationEnd = std::chrono::system_clock::now(); - assert(code.validate().first); - statistics_.schedulingTime = std::chrono::duration_cast(schedulingEnd - schedulingStart) @@ -203,25 +195,44 @@ class Compiler : protected Scheduler, avg, max); } #endif // SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG + + SPDLOG_DEBUG("Analyzing reuse..."); + const auto& reuseAnalysisStart = std::chrono::system_clock::now(); + const auto& reuseQubits = SELF.analyzeReuse(twoQubitGateLayers); + const auto& reuseAnalysisEnd = std::chrono::system_clock::now(); statistics_.reuseAnalysisTime = - std::chrono::duration_cast(reuseAnalysisEnd - - schedulingEnd) + std::chrono::duration_cast( + reuseAnalysisEnd - reuseAnalysisStart) .count(); SPDLOG_INFO("Time for reuse analysis: {}us", statistics_.reuseAnalysisTime); + + SPDLOG_DEBUG("Synthesizing layout..."); + const auto& layoutSynthesisStart = std::chrono::system_clock::now(); + const auto& [placement, routing] = LayoutSynthesizer::synthesize( + qComp.getNqubits(), twoQubitGateLayers, reuseQubits); + const auto& layoutSynthesisEnd = std::chrono::system_clock::now(); statistics_.layoutSynthesisTime = std::chrono::duration_cast( - layoutSynthesisEnd - reuseAnalysisEnd) + layoutSynthesisEnd - layoutSynthesisStart) .count(); statistics_.layoutSynthesizerStatistics = SELF.getLayoutSynthesisStatistics(); SPDLOG_INFO("Time for layout synthesis: {}us", statistics_.layoutSynthesisTime); + + SPDLOG_DEBUG("Generating code..."); + const auto& codeGenerationStart = std::chrono::system_clock::now(); + NAComputation code = + SELF.generate(singleQubitGateLayers, placement, routing); + const auto& codeGenerationEnd = std::chrono::system_clock::now(); + assert(code.validate().first); statistics_.codeGenerationTime = std::chrono::duration_cast( - codeGenerationEnd - layoutSynthesisEnd) + codeGenerationEnd - codeGenerationStart) .count(); SPDLOG_INFO("Time for code generation: {}us", statistics_.codeGenerationTime); + statistics_.totalTime = std::chrono::duration_cast( codeGenerationEnd - schedulingStart) @@ -254,7 +265,7 @@ class RoutingAgnosticCompiler final RoutingAgnosticCompiler(const Architecture& architecture, const Config& config) : Compiler(architecture, config) {} - RoutingAgnosticCompiler(const Architecture& architecture) + explicit RoutingAgnosticCompiler(const Architecture& architecture) : Compiler(architecture) {} }; @@ -275,7 +286,30 @@ class RoutingAwareCompiler final public: RoutingAwareCompiler(const Architecture& architecture, const Config& config) : Compiler(architecture, config) {} - RoutingAwareCompiler(const Architecture& architecture) + explicit RoutingAwareCompiler(const Architecture& architecture) + : Compiler(architecture) {} +}; + +class RelaxedRoutingAwareSynthesizer + : public PlaceAndRouteSynthesizer { +public: + RelaxedRoutingAwareSynthesizer(const Architecture& architecture, + const Config& config) + : PlaceAndRouteSynthesizer(architecture, config) {} + explicit RelaxedRoutingAwareSynthesizer(const Architecture& architecture) + : PlaceAndRouteSynthesizer(architecture) {} +}; +class RelaxedRoutingAwareCompiler final + : public Compiler { +public: + RelaxedRoutingAwareCompiler(const Architecture& architecture, + const Config& config) + : Compiler(architecture, config) {} + explicit RelaxedRoutingAwareCompiler(const Architecture& architecture) : Compiler(architecture) {} }; } // namespace na::zoned diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp new file mode 100644 index 000000000..fb81ed744 --- /dev/null +++ b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "ir/Definitions.hpp" +#include "na/zoned/Architecture.hpp" +#include "na/zoned/Types.hpp" +#include "na/zoned/layout_synthesizer/router/RouterBase.hpp" + +#include +#include +#include +#include +#include + +namespace na::zoned { + +/** + * This class implements the default Router for the zoned neutral atom compiler + * that forms groups of parallel movements by calculating a maximal independent + * set. + */ +class RelaxedIndependentSetRouter : public RouterBase { + std::reference_wrapper architecture_; + +public: + /** + * The configuration of the RelaxedIndependentSetRouter + * @note RelaxedIndependentSetRouter does not have any configuration + * parameters. + */ + struct Config { + // todo(yannick): add docstring + double preferSplit = 4.0; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, preferSplit); + }; + +private: + /// The configuration of the relaxed independent set router + Config config_; + +public: + /// Create a RelaxedIndependentSetRouter + RelaxedIndependentSetRouter(const Architecture& architecture, + const Config& config) + : architecture_(architecture), config_(config) {} + /** + * Given the computed placement, compute a possible routing. + * @details For this task, all movements are put in a conflict graph where an + * edge indicates that two atoms (nodes) cannot be moved together. The atoms + * are sorted by their distance in decreasing order such that atoms with + * larger distance are routed first and hopefully lead to more homogenous + * routing groups with similar movement distances within one group. + * @param placement is a vector of the atoms' placement at every layer + * @return the routing, i.e., for every transition between two placements a + * vector of groups containing atoms that can be moved simultaneously + */ + [[nodiscard]] auto route(const std::vector& placement) const + -> std::vector; + +private: + /** + * Creates the conflict graph. + * @details Atom/qubit indices are the nodes. Two nodes are connected if their + * corresponding move with respect to the given @p start- and @p + * targetPlacement stand in conflict with each other. The graph is + * represented as adjacency lists. + * @param atomsToMove are all atoms corresponding to nodes in the graph + * @param startPlacement is the start placement of all atoms as a mapping from + * atoms to their sites + * @param targetPlacement is the target placement of the atoms + * @return the conflict graph as an unordered_map, where the keys are the + * nodes and the values are vectors of their neighbors + */ + [[nodiscard]] auto + createConflictGraph(const std::vector& atomsToMove, + const Placement& startPlacement, + const Placement& targetPlacement) const + -> std::unordered_map>; + [[nodiscard]] auto + createRelaxedConflictGraph(const std::vector& atomsToMove, + const Placement& startPlacement, + const Placement& targetPlacement) const + -> std::unordered_map< + qc::Qubit, std::vector>>>; + + /** + * Takes two sites, the start and target site and returns a 4D-vector of the + * form (x-start, y-start, x-end, y-end) where the corresponding x- and + * y-coordinates are the coordinates of the exact location of the given sites. + * @param start is the start site + * @param target is the target site + * @return is the 4D-vector containing the exact site locations + */ + [[nodiscard]] auto + getMovementVector(const std::tuple& start, + const std::tuple& target) const + -> std::tuple; + + /** + * Check whether two movements are compatible, i.e., the topological order + * of the moved atoms remain the same. + * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) + * @param w is the other 4D-vector of the form (x-start, y-start, x-end, + * y-end) + * @return true, if the given movement vectors are compatible, otherwise false + */ + [[nodiscard]] static auto + isCompatibleMovement(const std::tuple& v, + const std::tuple& w) + -> bool; + // todo(yannick): add docstring + [[nodiscard]] auto isRelaxedIncompatibleMovement( + const std::tuple& v, + const std::tuple& w) const + -> std::optional>; +}; +} // namespace na::zoned diff --git a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp new file mode 100644 index 000000000..adb51510b --- /dev/null +++ b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp" + +#include "ir/Definitions.hpp" +#include "na/zoned/Architecture.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace na::zoned { +auto RelaxedIndependentSetRouter::createConflictGraph( + const std::vector& atomsToMove, const Placement& startPlacement, + const Placement& targetPlacement) const + -> std::unordered_map> { + std::unordered_map> conflictGraph; + for (auto atomIt = atomsToMove.cbegin(); atomIt != atomsToMove.cend(); + ++atomIt) { + const auto& atom = *atomIt; + const auto& atomMovementVector = + getMovementVector(startPlacement[atom], targetPlacement[atom]); + for (auto neighborIt = atomIt + 1; neighborIt != atomsToMove.cend(); + ++neighborIt) { + const auto& neighbor = *neighborIt; + const auto& neighborMovementVector = getMovementVector( + startPlacement[neighbor], targetPlacement[neighbor]); + if (!isCompatibleMovement(atomMovementVector, neighborMovementVector)) { + conflictGraph.try_emplace(atom).first->second.emplace_back(neighbor); + conflictGraph.try_emplace(neighbor).first->second.emplace_back(atom); + } + } + } + return conflictGraph; +} +auto RelaxedIndependentSetRouter::createRelaxedConflictGraph( + const std::vector& atomsToMove, const Placement& startPlacement, + const Placement& targetPlacement) const + -> std::unordered_map< + qc::Qubit, std::vector>>> { + std::unordered_map>>> + conflictGraph; + for (auto atomIt = atomsToMove.cbegin(); atomIt != atomsToMove.cend(); + ++atomIt) { + const auto& atom = *atomIt; + const auto& atomMovementVector = + getMovementVector(startPlacement[atom], targetPlacement[atom]); + for (auto neighborIt = atomIt + 1; neighborIt != atomsToMove.cend(); + ++neighborIt) { + const auto& neighbor = *neighborIt; + const auto& neighborMovementVector = getMovementVector( + startPlacement[neighbor], targetPlacement[neighbor]); + if (const auto& edge = isRelaxedIncompatibleMovement( + atomMovementVector, neighborMovementVector); + edge.has_value()) { + conflictGraph.try_emplace(atom).first->second.emplace_back(neighbor, + *edge); + conflictGraph.try_emplace(neighbor).first->second.emplace_back(atom, + *edge); + } + } + } + return conflictGraph; +} +auto RelaxedIndependentSetRouter::getMovementVector( + const std::tuple& start, + const std::tuple& target) const + -> std::tuple { + const auto& [startSLM, startRow, startColumn] = start; + const auto& [startX, startY] = + architecture_.get().exactSLMLocation(startSLM, startRow, startColumn); + const auto& [targetSLM, targetRow, targetColumn] = target; + const auto& [targetX, targetY] = + architecture_.get().exactSLMLocation(targetSLM, targetRow, targetColumn); + return std::make_tuple(startX, startY, targetX, targetY); +} +auto RelaxedIndependentSetRouter::isCompatibleMovement( + const std::tuple& v, + const std::tuple& w) -> bool { + const auto& [v0, v1, v2, v3] = v; + const auto& [w0, w1, w2, w3] = w; + if ((v0 == w0) != (v2 == w2)) { + return false; + } + if ((v0 < w0) != (v2 < w2)) { + return false; + } + if ((v1 == w1) != (v3 == w3)) { + return false; + } + if ((v1 < w1) != (v3 < w3)) { + return false; + } + return true; +} +namespace { +auto sumCubeRootsCubed(const double a, const double b) -> double { + double x = std::cbrt(a); + double y = std::cbrt(b); + // (x+y)^3 = a + b + 3*x*y*(x+y) + return a + b + 3.0 * x * y * (x + y); +} +auto subCubeRootsCubed(const double a, const double b) -> double { + double x = std::cbrt(a); + double y = std::cbrt(b); + // (x-y)^3 = a - b + 3*x*y*(y-x) + return a - b + 3.0 * x * y * (y - x); +} +} // namespace +auto RelaxedIndependentSetRouter::isRelaxedIncompatibleMovement( + const std::tuple& v, + const std::tuple& w) const + -> std::optional> { + const auto& [v0, v1, v2, v3] = v; + const auto& [w0, w1, w2, w3] = w; + if ((v0 == w0) != (v2 == w2)) { + return std::optional{std::optional{}}; + } + if ((v1 == w1) != (v3 == w3)) { + return std::optional{std::optional{}}; + } + if ((v0 < w0) != (v2 < w2) && (v1 < w1) != (v3 < w3)) { + return sumCubeRootsCubed( + static_cast( + std::abs(static_cast(v0) - static_cast(w0)) + + std::abs(static_cast(v2) - static_cast(w2))), + static_cast( + std::abs(static_cast(v1) - static_cast(w1)) + + std::abs(static_cast(v3) - static_cast(w3)))); + } + if ((v0 < w0) != (v2 < w2)) { + return static_cast( + std::abs(static_cast(v0) - static_cast(w0)) + + std::abs(static_cast(v2) - static_cast(w2))); + } + if ((v1 < w1) != (v3 < w3)) { + return static_cast( + std::abs(static_cast(v1) - static_cast(w1)) + + std::abs(static_cast(v3) - static_cast(w3))); + } + return std::nullopt; +} +auto RelaxedIndependentSetRouter::route( + const std::vector& placement) const -> std::vector { + std::vector routing; + // early return if no placement is given + if (placement.empty()) { + return routing; + } + for (auto it = placement.cbegin(); true;) { + const auto& startPlacement = *it; + if (++it == placement.cend()) { + break; + } + const auto& targetPlacement = *it; + std::set, std::greater<>> + atomsToMoveOrderedAscByDist; + std::unordered_map atomToDist; + assert(startPlacement.size() == targetPlacement.size()); + for (qc::Qubit atom = 0; atom < startPlacement.size(); ++atom) { + const auto& [startSLM, startRow, startColumn] = startPlacement[atom]; + const auto& [targetSLM, targetRow, targetColumn] = targetPlacement[atom]; + // if atom must be moved + if (&startSLM.get() != &targetSLM.get() || startRow != targetRow || + startColumn != targetColumn) { + const auto distance = + architecture_.get().distance(startSLM, startRow, startColumn, + targetSLM, targetRow, targetColumn); + atomsToMoveOrderedAscByDist.emplace(distance, atom); + atomToDist.emplace(atom, distance); + } + } + std::vector atomsToMove; + atomsToMove.reserve(atomsToMoveOrderedAscByDist.size()); + // put the atoms into the vector such they are ordered decreasingly by their + // movement distance + for (const auto& atomIt : atomsToMoveOrderedAscByDist) { + atomsToMove.emplace_back(atomIt.second); + } + const auto conflictGraph = + createConflictGraph(atomsToMove, startPlacement, targetPlacement); + const auto relaxedConflictGraph = createRelaxedConflictGraph( + atomsToMove, startPlacement, targetPlacement); + struct GroupInfo { + std::vector independentSet; + double maxDistance = 0.0; + std::unordered_map> + relaxedConflictingAtoms; + }; + std::list groups; + while (!atomsToMove.empty()) { + auto& group = groups.emplace_back(); + std::vector remainingAtoms; + std::unordered_set conflictingAtoms; + for (const auto& atom : atomsToMove) { + if (!conflictingAtoms.contains(atom)) { + // if the atom does not conflict with any atom that is already in the + // independent set, add it and mark its neighbors as conflicting + group.independentSet.emplace_back(atom); + const auto dist = atomToDist.at(atom); + if (group.maxDistance < dist) { + group.maxDistance = dist; + } + if (const auto conflictingNeighbors = conflictGraph.find(atom); + conflictingNeighbors != conflictGraph.end()) { + for (const auto neighbor : conflictingNeighbors->second) { + conflictingAtoms.emplace(neighbor); + } + assert(relaxedConflictGraph.contains(atom)); + for (const auto neighbor : relaxedConflictGraph.at(atom)) { + auto [conflictIt, success] = + group.relaxedConflictingAtoms.try_emplace(neighbor.first, + neighbor.second); + if (!success && conflictIt->second.has_value()) { + if (neighbor.second.has_value()) { + conflictIt->second = + std::max(*conflictIt->second, *neighbor.second); + } else { + conflictIt->second = std::nullopt; + } + } + } + } + } else { + // if an atom could not be put into the current independent set, add + // it to the remaining atoms + remainingAtoms.emplace_back(atom); + } + } + atomsToMove = remainingAtoms; + } + // try to merge rearrangement steps + for (auto groupIt = groups.rbegin(); groupIt != groups.rend();) { + const auto& independentSet = groupIt->independentSet; + std::unordered_map + atomToNewGroup; + // find best new group for each qubit in independent set and record costs + auto totalCost = 0.0; + auto totalCostCubed = 0.0; + bool foundNewGroupForAllAtoms = true; + for (const auto& atom : independentSet) { + bool foundNewGroup = false; + auto cost = std::numeric_limits::max(); + auto costCubed = std::numeric_limits::max(); + for (auto& group : groups | std::views::reverse) { + // filter current group + if (&group != &*groupIt) { + if (const auto conflictIt = + group.relaxedConflictingAtoms.find(atom); + conflictIt == group.relaxedConflictingAtoms.end()) { + const auto dist = atomToDist.at(atom); + if (group.maxDistance >= dist) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + cost = 0; + costCubed = 0; + break; + } + const auto diff = subCubeRootsCubed(dist, group.maxDistance); + if (costCubed > diff) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = diff; + cost = std::cbrt(diff); + } + } else if (conflictIt->second.has_value()) { + // can be added with additional cost because there is a strict + // conflict + const auto dist = atomToDist.at(atom); + if (group.maxDistance > dist) { + if (costCubed > *conflictIt->second) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = *conflictIt->second; + cost = std::cbrt(*conflictIt->second); + } + } else { + const auto c = sumCubeRootsCubed( + subCubeRootsCubed(dist, group.maxDistance), + *conflictIt->second); + if (costCubed > c) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = c; + cost = std::cbrt(c); + } + } + } + } + } + if (!foundNewGroup) { + foundNewGroupForAllAtoms = false; + break; + } + // Note the following identity to calculate the new cubed offset as + // offsetCostCubed' = (offsetCost + bestCost)^3 + // + // Identity: (x + y)^3 = x^3 + y^3 + 3xy(x+y) + totalCostCubed = costCubed + totalCostCubed + + 3 * cost * totalCost * (cost + totalCost); + totalCost += cost; + } + // if all atoms in independent set could be assigned to a new group and + // the offset cost is less than the cost for the current group which is + // proportional to the cubed maximum distance + if (foundNewGroupForAllAtoms && + groupIt->maxDistance > config_.preferSplit * totalCostCubed) { + std::ranges::for_each(atomToNewGroup, [&relaxedConflictGraph, + &atomToDist](const auto& pair) { + const auto& [atom, group] = pair; + // add atom to new group + group->independentSet.emplace_back(atom); + const auto dist = atomToDist.at(atom); + if (group->maxDistance < dist) { + group->maxDistance = dist; + } + if (const auto relaxedConflictingNeighbors = + relaxedConflictGraph.find(atom); + relaxedConflictingNeighbors != relaxedConflictGraph.end()) { + for (const auto neighbor : relaxedConflictingNeighbors->second) { + auto [conflictIt, success] = + group->relaxedConflictingAtoms.try_emplace(neighbor.first, + neighbor.second); + if (!success && conflictIt->second.has_value()) { + if (neighbor.second.has_value()) { + conflictIt->second = + std::max(*conflictIt->second, *neighbor.second); + } else { + conflictIt->second = std::nullopt; + } + } + } + } + }); + // erase the current group from the linked list of groups; note that + // a reverse pointer always points to the element in front of the + // current iterator position. + const auto& a = (++groupIt).base(); + const auto& b = groups.erase(a); + groupIt = std::make_reverse_iterator(b); + } else { + ++groupIt; + } + } + auto& currentRouting = routing.emplace_back(); + currentRouting.reserve(groups.size()); + for (auto& group : groups) { + currentRouting.emplace_back(std::move(group.independentSet)); + } + } + return routing; +} +} // namespace na::zoned diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 197bd6716..f74f74ae0 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -70,6 +70,24 @@ constexpr std::string_view routingAwareConfiguration = R"({ } } })"; +constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ + "logLevel" : 1, + "codeGeneratorConfig" : { + "warnUnsupportedGates" : false + }, + "layoutSynthesizerConfig" : { + "placerConfig" : { + "useWindow" : true, + "windowMinWidth" : 4, + "windowRatio" : 1.5, + "windowShare" : 0.6, + "deepeningFactor" : 0.6, + "deepeningValue" : 0.2, + "lookaheadFactor": 0.2, + "reuseLevel": 5.0 + } + } +})"; #define COMPILER_TEST(compiler_type, config) \ TEST(compiler_type##Test, ConstructorWithoutConfig) { \ Architecture architecture( \ @@ -121,12 +139,13 @@ constexpr std::string_view routingAwareConfiguration = R"({ ::testing::Values(TEST_CIRCUITS), /* Parameters to test with */ \ [](const ::testing::TestParamInfo& pinfo) { \ const auto& path = pinfo.param; \ - const auto& filename = path.substr(path.find_last_of("/") + 1); \ - return filename.substr(0, filename.find_last_of(".")); \ + const auto& filename = path.substr(path.find_last_of('/') + 1); \ + return filename.substr(0, filename.find_last_of('.')); \ }) /*============================== INSTANTIATIONS ==============================*/ COMPILER_TEST(RoutingAgnosticCompiler, routingAgnosticConfiguration); COMPILER_TEST(RoutingAwareCompiler, routingAwareConfiguration); +COMPILER_TEST(RelaxedRoutingAwareCompiler, relaxedRoutingAwareConfiguration); // Tests that the bug described in issue // https://github.com/munich-quantum-toolkit/qmap/issues/727 is fixed. From 16ec9237b3c3c53433f1cf11645fad32d97ca144 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:13:25 +0100 Subject: [PATCH 025/119] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20references=20fr?= =?UTF-8?q?om=20chrono=20variable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/na/zoned/Compiler.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/include/na/zoned/Compiler.hpp b/include/na/zoned/Compiler.hpp index 07319e1b4..6397b052c 100644 --- a/include/na/zoned/Compiler.hpp +++ b/include/na/zoned/Compiler.hpp @@ -163,9 +163,9 @@ class Compiler : protected Scheduler, // CodeQL was not very happy about the structural binding here, hence I // removed it. SPDLOG_DEBUG("Scheduling..."); - const auto& schedulingStart = std::chrono::system_clock::now(); + const auto schedulingStart = std::chrono::system_clock::now(); const auto& schedule = SELF.schedule(qComp); - const auto& schedulingEnd = std::chrono::system_clock::now(); + const auto schedulingEnd = std::chrono::system_clock::now(); const auto& singleQubitGateLayers = schedule.first; const auto& twoQubitGateLayers = schedule.second; statistics_.schedulingTime = @@ -197,9 +197,9 @@ class Compiler : protected Scheduler, #endif // SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG SPDLOG_DEBUG("Analyzing reuse..."); - const auto& reuseAnalysisStart = std::chrono::system_clock::now(); + const auto reuseAnalysisStart = std::chrono::system_clock::now(); const auto& reuseQubits = SELF.analyzeReuse(twoQubitGateLayers); - const auto& reuseAnalysisEnd = std::chrono::system_clock::now(); + const auto reuseAnalysisEnd = std::chrono::system_clock::now(); statistics_.reuseAnalysisTime = std::chrono::duration_cast( reuseAnalysisEnd - reuseAnalysisStart) @@ -207,10 +207,10 @@ class Compiler : protected Scheduler, SPDLOG_INFO("Time for reuse analysis: {}us", statistics_.reuseAnalysisTime); SPDLOG_DEBUG("Synthesizing layout..."); - const auto& layoutSynthesisStart = std::chrono::system_clock::now(); + const auto layoutSynthesisStart = std::chrono::system_clock::now(); const auto& [placement, routing] = LayoutSynthesizer::synthesize( qComp.getNqubits(), twoQubitGateLayers, reuseQubits); - const auto& layoutSynthesisEnd = std::chrono::system_clock::now(); + const auto layoutSynthesisEnd = std::chrono::system_clock::now(); statistics_.layoutSynthesisTime = std::chrono::duration_cast( layoutSynthesisEnd - layoutSynthesisStart) @@ -221,10 +221,10 @@ class Compiler : protected Scheduler, statistics_.layoutSynthesisTime); SPDLOG_DEBUG("Generating code..."); - const auto& codeGenerationStart = std::chrono::system_clock::now(); + const auto codeGenerationStart = std::chrono::system_clock::now(); NAComputation code = SELF.generate(singleQubitGateLayers, placement, routing); - const auto& codeGenerationEnd = std::chrono::system_clock::now(); + const auto codeGenerationEnd = std::chrono::system_clock::now(); assert(code.validate().first); statistics_.codeGenerationTime = std::chrono::duration_cast( From 2d46808a7e08ace172d74ee2ff98662a1f047aaa Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:22:31 +0100 Subject: [PATCH 026/119] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/RelaxedIndependentSetRouter.hpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp index fb81ed744..e60336780 100644 --- a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp @@ -38,8 +38,15 @@ class RelaxedIndependentSetRouter : public RouterBase { * parameters. */ struct Config { - // todo(yannick): add docstring - double preferSplit = 4.0; + /** + * @brief Threshold factor for group merging decisions during routing. + * @details First, a strict routing is computed resulting in a set of + * rearrangement groups. Afterward, some of those are merged with existing + * groups based on the relaxed constraints. Higher values of this + * parameter favor keeping groups separate; lower values favor merging. + * In particular, a value of 0.0 merges all possible groups. (Default: 1.0) + */ + double preferSplit = 1.0; NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, preferSplit); }; From 174041983288bfc6789b042caf2d80be0e4329d3 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:18:05 +0100 Subject: [PATCH 027/119] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/RelaxedIndependentSetRouter.hpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp index e60336780..800da186f 100644 --- a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp @@ -124,7 +124,16 @@ class RelaxedIndependentSetRouter : public RouterBase { isCompatibleMovement(const std::tuple& v, const std::tuple& w) -> bool; - // todo(yannick): add docstring + /** + * Check whether two movements are incompatible with respect to the relaxed + * routing constraints, i.e., moved atoms remain not on the same row (column). + * This is, however, independent of their topological order (i.e., relaxed). + * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) + * @param w is the other 4D-vector of the form (x-start, y-start, x-end, + * y-end) + * @return true, if the given movement vectors are incompatible, otherwise + * false + */ [[nodiscard]] auto isRelaxedIncompatibleMovement( const std::tuple& v, const std::tuple& w) const From 362bee61d968df62b06d1da3df57b3d06e72bd37 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:19:30 +0100 Subject: [PATCH 028/119] =?UTF-8?q?=F0=9F=8E=A8=20Make=20function=20static?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/router/RelaxedIndependentSetRouter.hpp | 4 ++-- .../layout_synthesizer/router/RelaxedIndependentSetRouter.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp index 800da186f..685f27405 100644 --- a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp @@ -134,9 +134,9 @@ class RelaxedIndependentSetRouter : public RouterBase { * @return true, if the given movement vectors are incompatible, otherwise * false */ - [[nodiscard]] auto isRelaxedIncompatibleMovement( + [[nodiscard]] static auto isRelaxedIncompatibleMovement( const std::tuple& v, - const std::tuple& w) const + const std::tuple& w) -> std::optional>; }; } // namespace na::zoned diff --git a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp index adb51510b..4b9685166 100644 --- a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp @@ -125,7 +125,7 @@ auto subCubeRootsCubed(const double a, const double b) -> double { } // namespace auto RelaxedIndependentSetRouter::isRelaxedIncompatibleMovement( const std::tuple& v, - const std::tuple& w) const + const std::tuple& w) -> std::optional> { const auto& [v0, v1, v2, v3] = v; const auto& [w0, w1, w2, w3] = w; From 9575f6611b2f4464049dc1509c34179edd996a97 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:22:27 +0100 Subject: [PATCH 029/119] =?UTF-8?q?=F0=9F=8E=A8=20More=20readable=20altern?= =?UTF-8?q?ative?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/router/IndependentSetRouter.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index c8a180e9e..772de6bd6 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -87,12 +87,9 @@ auto IndependentSetRouter::route(const std::vector& placement) const if (placement.empty()) { return routing; } - for (auto it = placement.cbegin(); true;) { - const auto& startPlacement = *it; - if (++it == placement.cend()) { - break; - } - const auto& targetPlacement = *it; + for (size_t i = 0; i + 1 < placement.size(); ++i) { + const auto& startPlacement = placement[i]; + const auto& targetPlacement = placement[i + 1]; std::set, std::greater<>> atomsToMoveOrderedAscByDist; assert(startPlacement.size() == targetPlacement.size()); From c4e58a90d14c3a55d707bec89d4145a5922f09af Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:23:38 +0100 Subject: [PATCH 030/119] =?UTF-8?q?=F0=9F=93=9D=20Add=20addtional=20commen?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/router/RelaxedIndependentSetRouter.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp index 4b9685166..6e0fe0ebe 100644 --- a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp @@ -351,6 +351,8 @@ auto RelaxedIndependentSetRouter::route( // erase the current group from the linked list of groups; note that // a reverse pointer always points to the element in front of the // current iterator position. + // After erasing, we create a new reverse iterator pointing to the + // same logical position in the remaining list. const auto& a = (++groupIt).base(); const auto& b = groups.erase(a); groupIt = std::make_reverse_iterator(b); From 00a412196f0cbc4351781fbb729ebe90f71ae1e3 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:28:07 +0100 Subject: [PATCH 031/119] =?UTF-8?q?=F0=9F=94=A7=20Add=20missing=20config?= =?UTF-8?q?=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_compiler.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 5244cd5df..024a36e9b 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -49,7 +49,6 @@ constexpr std::string_view routingAgnosticConfiguration = R"({ } }, "codeGeneratorConfig" : { - "parkingOffset" : 1, "warnUnsupportedGates" : false } })"; @@ -86,6 +85,9 @@ constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ "deepeningValue" : 0.2, "lookaheadFactor": 0.2, "reuseLevel": 5.0 + }, + "routerConfig" : { + "preferSplit" : 0.0 } } })"; From 1eac09f6c8474271351a26d1a27199d0ac999c75 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:30:15 +0100 Subject: [PATCH 032/119] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20platform=20com?= =?UTF-8?q?patibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_compiler.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 024a36e9b..37174a3c5 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -141,9 +141,8 @@ constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ compiler_type##Test, /* Test suite name */ \ ::testing::Values(TEST_CIRCUITS), /* Parameters to test with */ \ [](const ::testing::TestParamInfo& pinfo) { \ - const auto& path = pinfo.param; \ - const auto& filename = path.substr(path.find_last_of('/') + 1); \ - return filename.substr(0, filename.find_last_of('.')); \ + const std::filesystem::path path(pinfo.param); \ + return path.stem().string(); \ }) /*============================== INSTANTIATIONS ==============================*/ COMPILER_TEST(RoutingAgnosticCompiler, routingAgnosticConfiguration); From 85212ba1ee5ff4f5986154a2c8c75eabcba6eec9 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:14:24 +0100 Subject: [PATCH 033/119] =?UTF-8?q?=F0=9F=8E=A8=20Add=20override=20annotat?= =?UTF-8?q?ions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlaceAndRouteSynthesizer.hpp | 8 ++-- .../layout_synthesizer/placer/AStarPlacer.hpp | 2 +- .../placer/VertexMatchingPlacer.hpp | 2 +- .../router/IndependentSetRouter.hpp | 2 +- .../router/RelaxedIndependentSetRouter.hpp | 41 +++++++++++++++++-- .../VertexMatchingReuseAnalyzer.hpp | 2 +- .../router/RelaxedIndependentSetRouter.cpp | 34 +++++++-------- 7 files changed, 64 insertions(+), 27 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/PlaceAndRouteSynthesizer.hpp b/include/na/zoned/layout_synthesizer/PlaceAndRouteSynthesizer.hpp index 4b0d63575..9010c104c 100644 --- a/include/na/zoned/layout_synthesizer/PlaceAndRouteSynthesizer.hpp +++ b/include/na/zoned/layout_synthesizer/PlaceAndRouteSynthesizer.hpp @@ -97,9 +97,11 @@ class PlaceAndRouteSynthesizer : public LayoutSynthesizerBase, : PlaceAndRouteSynthesizer(architecture, Config{}) {} public: - [[nodiscard]] auto synthesize( - size_t nQubits, const std::vector& twoQubitGateLayers, - const std::vector>& reuseQubits) -> Layout { + [[nodiscard]] auto + synthesize(size_t nQubits, + const std::vector& twoQubitGateLayers, + const std::vector>& reuseQubits) + -> Layout override { const auto& placementStart = std::chrono::system_clock::now(); const auto& placement = SELF.place(nQubits, twoQubitGateLayers, reuseQubits); diff --git a/include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp index d4b845f52..7236d0396 100644 --- a/include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp @@ -303,7 +303,7 @@ class AStarPlacer : public PlacerBase { place(size_t nQubits, const std::vector& twoQubitGateLayers, const std::vector>& reuseQubits) - -> std::vector; + -> std::vector override; private: /** diff --git a/include/na/zoned/layout_synthesizer/placer/VertexMatchingPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/VertexMatchingPlacer.hpp index a81ae78d3..80b82b30f 100644 --- a/include/na/zoned/layout_synthesizer/placer/VertexMatchingPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/VertexMatchingPlacer.hpp @@ -99,7 +99,7 @@ class VertexMatchingPlacer : public PlacerBase { place(size_t nQubits, const std::vector& twoQubitGateLayers, const std::vector>& reuseQubits) - -> std::vector; + -> std::vector override; private: /// Generate qubit initial layout diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index fd0066318..00a3b1c6d 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -60,7 +60,7 @@ class IndependentSetRouter : public RouterBase { * vector of groups containing atoms that can be moved simultaneously */ [[nodiscard]] auto route(const std::vector& placement) const - -> std::vector; + -> std::vector override; private: /** diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp index 685f27405..25ef9099f 100644 --- a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp @@ -71,7 +71,7 @@ class RelaxedIndependentSetRouter : public RouterBase { * vector of groups containing atoms that can be moved simultaneously */ [[nodiscard]] auto route(const std::vector& placement) const - -> std::vector; + -> std::vector override; private: /** @@ -124,6 +124,41 @@ class RelaxedIndependentSetRouter : public RouterBase { isCompatibleMovement(const std::tuple& v, const std::tuple& w) -> bool; + /** + * This struct indicates whether two movements are strictly compatible, + * relaxed compatible together with the corresponding merging cost, or + * (completely) incompatible. + */ + struct MovementCompatibility { + enum class Status : uint8_t { + StrictlyCompatible, // Movements can proceed in parallel + RelaxedCompatible, // Can be merged with a cost + Incompatible // Cannot be merged at all + }; + + /// Indicates the type of compatibility + Status status; + /** + * In the case of `RelaxedIncompatible`, the cost to merge the two + * movements + */ + std::optional mergeCost; + + /// Factory methods for strict compatibility + [[nodiscard]] static auto strictlyCompatible() -> MovementCompatibility { + return {.status = Status::StrictlyCompatible, .mergeCost = std::nullopt}; + } + + /// Factory method for incompatibility + [[nodiscard]] static auto incompatible() -> MovementCompatibility { + return {.status = Status::Incompatible, .mergeCost = std::nullopt}; + } + + [[nodiscard]] static auto relaxedCompatible(double cost) + -> MovementCompatibility { + return {.status = Status::RelaxedCompatible, .mergeCost = cost}; + } + }; /** * Check whether two movements are incompatible with respect to the relaxed * routing constraints, i.e., moved atoms remain not on the same row (column). @@ -134,9 +169,9 @@ class RelaxedIndependentSetRouter : public RouterBase { * @return true, if the given movement vectors are incompatible, otherwise * false */ - [[nodiscard]] static auto isRelaxedIncompatibleMovement( + [[nodiscard]] static auto isRelaxedCompatibleMovement( const std::tuple& v, const std::tuple& w) - -> std::optional>; + -> MovementCompatibility; }; } // namespace na::zoned diff --git a/include/na/zoned/reuse_analyzer/VertexMatchingReuseAnalyzer.hpp b/include/na/zoned/reuse_analyzer/VertexMatchingReuseAnalyzer.hpp index e35ac79a0..0668fc377 100644 --- a/include/na/zoned/reuse_analyzer/VertexMatchingReuseAnalyzer.hpp +++ b/include/na/zoned/reuse_analyzer/VertexMatchingReuseAnalyzer.hpp @@ -58,7 +58,7 @@ class VertexMatchingReuseAnalyzer : public ReuseAnalyzerBase { /// Analyze the reuse of qubits in the given two-qubit gate layers. [[nodiscard]] auto analyzeReuse(const std::vector& twoQubitGateLayers) - -> std::vector>; + -> std::vector> override; private: /** diff --git a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp index 6e0fe0ebe..02c1873c1 100644 --- a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp @@ -66,13 +66,13 @@ auto RelaxedIndependentSetRouter::createRelaxedConflictGraph( const auto& neighbor = *neighborIt; const auto& neighborMovementVector = getMovementVector( startPlacement[neighbor], targetPlacement[neighbor]); - if (const auto& edge = isRelaxedIncompatibleMovement( + if (const auto& comp = isRelaxedCompatibleMovement( atomMovementVector, neighborMovementVector); - edge.has_value()) { - conflictGraph.try_emplace(atom).first->second.emplace_back(neighbor, - *edge); - conflictGraph.try_emplace(neighbor).first->second.emplace_back(atom, - *edge); + comp.status != MovementCompatibility::Status::Incompatible) { + conflictGraph.try_emplace(atom).first->second.emplace_back( + neighbor, comp.mergeCost); + conflictGraph.try_emplace(neighbor).first->second.emplace_back( + atom, comp.mergeCost); } } } @@ -123,38 +123,38 @@ auto subCubeRootsCubed(const double a, const double b) -> double { return a - b + 3.0 * x * y * (y - x); } } // namespace -auto RelaxedIndependentSetRouter::isRelaxedIncompatibleMovement( +auto RelaxedIndependentSetRouter::isRelaxedCompatibleMovement( const std::tuple& v, const std::tuple& w) - -> std::optional> { + -> MovementCompatibility { const auto& [v0, v1, v2, v3] = v; const auto& [w0, w1, w2, w3] = w; if ((v0 == w0) != (v2 == w2)) { - return std::optional{std::optional{}}; + return MovementCompatibility::incompatible(); } if ((v1 == w1) != (v3 == w3)) { - return std::optional{std::optional{}}; + return MovementCompatibility::incompatible(); } if ((v0 < w0) != (v2 < w2) && (v1 < w1) != (v3 < w3)) { - return sumCubeRootsCubed( + return MovementCompatibility::relaxedCompatible(sumCubeRootsCubed( static_cast( std::abs(static_cast(v0) - static_cast(w0)) + std::abs(static_cast(v2) - static_cast(w2))), static_cast( std::abs(static_cast(v1) - static_cast(w1)) + - std::abs(static_cast(v3) - static_cast(w3)))); + std::abs(static_cast(v3) - static_cast(w3))))); } if ((v0 < w0) != (v2 < w2)) { - return static_cast( + return MovementCompatibility::relaxedCompatible(static_cast( std::abs(static_cast(v0) - static_cast(w0)) + - std::abs(static_cast(v2) - static_cast(w2))); + std::abs(static_cast(v2) - static_cast(w2)))); } if ((v1 < w1) != (v3 < w3)) { - return static_cast( + return MovementCompatibility::relaxedCompatible(static_cast( std::abs(static_cast(v1) - static_cast(w1)) + - std::abs(static_cast(v3) - static_cast(w3))); + std::abs(static_cast(v3) - static_cast(w3)))); } - return std::nullopt; + return MovementCompatibility::strictlyCompatible(); } auto RelaxedIndependentSetRouter::route( const std::vector& placement) const -> std::vector { From d19af9a4b39345247bf014f5a85e390c0e10e990 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:20:26 +0100 Subject: [PATCH 034/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20minor=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/RelaxedIndependentSetRouter.hpp | 2 +- .../router/RelaxedIndependentSetRouter.cpp | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp index 25ef9099f..9068191cb 100644 --- a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp @@ -154,7 +154,7 @@ class RelaxedIndependentSetRouter : public RouterBase { return {.status = Status::Incompatible, .mergeCost = std::nullopt}; } - [[nodiscard]] static auto relaxedCompatible(double cost) + [[nodiscard]] static auto relaxedCompatible(const double cost) -> MovementCompatibility { return {.status = Status::RelaxedCompatible, .mergeCost = cost}; } diff --git a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp index 02c1873c1..a39774750 100644 --- a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp @@ -68,7 +68,7 @@ auto RelaxedIndependentSetRouter::createRelaxedConflictGraph( startPlacement[neighbor], targetPlacement[neighbor]); if (const auto& comp = isRelaxedCompatibleMovement( atomMovementVector, neighborMovementVector); - comp.status != MovementCompatibility::Status::Incompatible) { + comp.status != MovementCompatibility::Status::StrictlyCompatible) { conflictGraph.try_emplace(atom).first->second.emplace_back( neighbor, comp.mergeCost); conflictGraph.try_emplace(neighbor).first->second.emplace_back( @@ -129,10 +129,7 @@ auto RelaxedIndependentSetRouter::isRelaxedCompatibleMovement( -> MovementCompatibility { const auto& [v0, v1, v2, v3] = v; const auto& [w0, w1, w2, w3] = w; - if ((v0 == w0) != (v2 == w2)) { - return MovementCompatibility::incompatible(); - } - if ((v1 == w1) != (v3 == w3)) { + if (((v0 == w0) != (v2 == w2)) || ((v1 == w1) != (v3 == w3))) { return MovementCompatibility::incompatible(); } if ((v0 < w0) != (v2 < w2) && (v1 < w1) != (v3 < w3)) { @@ -190,8 +187,8 @@ auto RelaxedIndependentSetRouter::route( atomsToMove.reserve(atomsToMoveOrderedAscByDist.size()); // put the atoms into the vector such they are ordered decreasingly by their // movement distance - for (const auto& atomIt : atomsToMoveOrderedAscByDist) { - atomsToMove.emplace_back(atomIt.second); + for (const auto& val : atomsToMoveOrderedAscByDist | std::views::values) { + atomsToMove.emplace_back(val); } const auto conflictGraph = createConflictGraph(atomsToMove, startPlacement, targetPlacement); From 77ebe3b3d3656cd1e515566a6179dd736a8891bf Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:30:10 +0100 Subject: [PATCH 035/119] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20AStarPlacer=20t?= =?UTF-8?q?o=20HeuristicPlacer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 2 +- include/na/zoned/Compiler.hpp | 6 +- .../{AStarPlacer.hpp => HeuristicPlacer.hpp} | 10 +-- .../{AStarPlacer.cpp => HeuristicPlacer.cpp} | 73 +++++++++--------- test/na/zoned/test_a_star_placer.cpp | 76 ++++++++++--------- 5 files changed, 87 insertions(+), 80 deletions(-) rename include/na/zoned/layout_synthesizer/placer/{AStarPlacer.hpp => HeuristicPlacer.hpp} (98%) rename src/na/zoned/layout_synthesizer/placer/{AStarPlacer.cpp => HeuristicPlacer.cpp} (96%) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 2588957eb..2e054e29c 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 diff --git a/include/na/zoned/Compiler.hpp b/include/na/zoned/Compiler.hpp index 6397b052c..b15af08da 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 "layout_synthesizer/router/RelaxedIndependentSetRouter.hpp" @@ -270,7 +270,7 @@ class RoutingAgnosticCompiler final }; class RoutingAwareSynthesizer - : public PlaceAndRouteSynthesizer { public: RoutingAwareSynthesizer(const Architecture& architecture, @@ -292,7 +292,7 @@ class RoutingAwareCompiler final class RelaxedRoutingAwareSynthesizer : public PlaceAndRouteSynthesizer { public: RelaxedRoutingAwareSynthesizer(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 98% rename from include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp rename to include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 7236d0396..0b83980df 100644 --- a/include/na/zoned/layout_synthesizer/placer/AStarPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -41,11 +41,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 HeuristicPlacerPlacerTest_AStarSearch_Test; using DiscreteSite = std::array; using CompatibilityGroup = std::array, 2>; @@ -290,7 +290,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 diff --git a/src/na/zoned/layout_synthesizer/placer/AStarPlacer.cpp b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp similarity index 96% rename from src/na/zoned/layout_synthesizer/placer/AStarPlacer.cpp rename to src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp index 1dfa4f11f..b70150ac7 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" @@ -40,7 +40,7 @@ namespace na::zoned { template -auto AStarPlacer::aStarTreeSearch( +auto HeuristicPlacer::aStarTreeSearch( const Node& start, const std::function>( const Node&)>& getNeighbors, @@ -124,13 +124,15 @@ auto AStarPlacer::aStarTreeSearch( "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; @@ -160,7 +162,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; @@ -203,7 +205,7 @@ auto AStarPlacer::discretizeNonOccupiedStorageSites( return std::pair{rowIndices, columnIndices}; } -auto AStarPlacer::discretizeNonOccupiedEntanglementSites( +auto HeuristicPlacer::discretizeNonOccupiedEntanglementSites( const SiteSet& occupiedSites) const -> std::pair, RowColumnMap> { std::map rows; @@ -256,7 +258,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 +285,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, @@ -298,7 +300,7 @@ auto AStarPlacer::makeIntermediatePlacement( 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 +353,7 @@ auto AStarPlacer::addGateOption( } } -auto AStarPlacer::placeGatesInEntanglementZone( +auto HeuristicPlacer::placeGatesInEntanglementZone( const Placement& previousPlacement, const std::unordered_set& reuseQubits, const TwoQubitGateLayer& twoQubitGates, @@ -740,7 +742,7 @@ auto AStarPlacer::placeGatesInEntanglementZone( return currentPlacement; } -auto AStarPlacer::placeAtomsInStorageZone( +auto HeuristicPlacer::placeAtomsInStorageZone( const Placement& previousPlacement, const std::unordered_set& reuseQubits, const TwoQubitGateLayer& twoQubitGates, @@ -1145,7 +1147,7 @@ auto AStarPlacer::placeAtomsInStorageZone( 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 +1155,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 +1163,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 +1190,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; @@ -1234,11 +1236,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; @@ -1279,9 +1281,9 @@ auto AStarPlacer::getHeuristic(const std::vector& gateJobs, return heuristic; } -auto AStarPlacer::getNeighbors(std::deque>& nodes, - const std::vector& atomJobs, - const AtomNode& node) +auto HeuristicPlacer::getNeighbors(std::deque>& nodes, + const std::vector& atomJobs, + const AtomNode& node) -> std::vector> { const size_t atomToBePlacedNext = node.level; const auto& atomJob = atomJobs[atomToBePlacedNext]; @@ -1315,9 +1317,9 @@ auto AStarPlacer::getNeighbors(std::deque>& nodes, return neighbors; } -auto AStarPlacer::getNeighbors(std::deque>& nodes, - const std::vector& gateJobs, - const GateNode& node) +auto HeuristicPlacer::getNeighbors(std::deque>& nodes, + const std::vector& gateJobs, + const GateNode& node) -> std::vector> { const size_t gateToBePlacedNext = node.level; const auto& gateJob = gateJobs[gateToBePlacedNext]; @@ -1360,7 +1362,7 @@ auto AStarPlacer::getNeighbors(std::deque>& nodes, return neighbors; } -auto AStarPlacer::checkCompatibilityWithGroup( +auto HeuristicPlacer::checkCompatibilityWithGroup( const uint8_t key, const uint8_t value, const std::map& group) -> std::optional< @@ -1401,7 +1403,7 @@ auto AStarPlacer::checkCompatibilityWithGroup( 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) @@ -1437,7 +1439,8 @@ auto AStarPlacer::checkCompatibilityAndAddPlacement( return false; } -AStarPlacer::AStarPlacer(const Architecture& architecture, const Config& config) +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(); @@ -1455,7 +1458,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) diff --git a/test/na/zoned/test_a_star_placer.cpp b/test/na/zoned/test_a_star_placer.cpp index 49337c7f3..5a063a3ff 100644 --- a/test/na/zoned/test_a_star_placer.cpp +++ b/test/na/zoned/test_a_star_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 @@ -53,12 +53,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) { @@ -221,7 +221,7 @@ TEST_F(AStarPlacerPlaceTest, TwoTwoQubitLayerReuse) { } TEST(AStarPlacerTest, NoSolution) { Architecture architecture(Architecture::fromJSONString(architectureJson)); - AStarPlacer::Config config = R"({ + HeuristicPlacer::Config config = R"({ "useWindow": true, "windowMinWidth": 0, "windowRatio": 1.0, @@ -231,7 +231,7 @@ TEST(AStarPlacerTest, NoSolution) { "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( @@ -242,7 +242,7 @@ TEST(AStarPlacerTest, NoSolution) { } TEST(AStarPlacerTest, LimitSpace) { Architecture architecture(Architecture::fromJSONString(architectureJson)); - AStarPlacer placer(architecture, R"({ + HeuristicPlacer placer(architecture, R"({ "useWindow": true, "windowMinWidth": 4, "windowRatio": 1.5, @@ -263,7 +263,7 @@ TEST(AStarPlacerTest, LimitSpace) { } TEST(AStarPlacerTest, WindowExpansion) { Architecture architecture(Architecture::fromJSONString(architectureJson)); - AStarPlacer placer(architecture, R"({ + HeuristicPlacer placer(architecture, R"({ "useWindow": true, "windowMinWidth": 1, "windowRatio": 1.0, @@ -303,7 +303,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>>{}, @@ -337,10 +337,10 @@ TEST(AStarPlacerTest, AStarSearch) { // ┌─────┐ ┌─────┐ ┌Goal=┐ ┌─────┐ // │ 12 ├─────→ │ 13 ├─────→ │ 14 ├─────→ │ 15 │ // └─────┘ └─────┘ └=====┘ └─────┘ - const std::vector nodes(16); + const std::vector nodes(16); std::unordered_map< - const AStarPlacer::AtomNode*, - std::vector>> + const HeuristicPlacer::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])}}, @@ -357,31 +357,35 @@ TEST(AStarPlacerTest, AStarSearch) { {&nodes[13], {std::cref(nodes[14])}}, {&nodes[14], {std::cref(nodes[15])}}, {&nodes[15], {}}}; - const auto path = (AStarPlacer::aStarTreeSearch( - /* start: */ - nodes[0], - /* getNeighbors: */ - [&neighbors](const AStarPlacer::AtomNode& node) - -> std::vector> { - return neighbors.at(&node); - }, - /* isGoal: */ - [&nodes](const AStarPlacer::AtomNode& node) -> bool { - return &node == &nodes[14]; - }, - /* getCost: */ - [](const AStarPlacer::AtomNode& /* 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)); + const auto path = + (HeuristicPlacer::aStarTreeSearch( + /* start: */ + nodes[0], + /* getNeighbors: */ + [&neighbors](const HeuristicPlacer::AtomNode& node) + -> std::vector< + std::reference_wrapper> { + return neighbors.at(&node); + }, + /* isGoal: */ + [&nodes](const HeuristicPlacer::AtomNode& node) -> bool { + return &node == &nodes[14]; + }, + /* getCost: */ + [](const HeuristicPlacer::AtomNode& /* unused */) -> double { + return 1.0; + }, + /* getHeuristic: */ + [&nodes](const HeuristicPlacer::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; + std::vector pathNodes; for (const auto& node : path) { pathNodes.emplace_back(&node.get()); } From f5607f656ad8f0395c4c418ab450b9d4e26b313f Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:01:42 +0100 Subject: [PATCH 036/119] =?UTF-8?q?=F0=9F=9A=9A=20Update=20header=20to=20c?= =?UTF-8?q?ombine=20astar=20and=20ids=20placer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placer/HeuristicPlacer.hpp | 339 ++++++++++++++++-- 1 file changed, 309 insertions(+), 30 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 0b83980df..8a9c05554 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -81,7 +82,7 @@ class HeuristicPlacer : 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 +105,16 @@ class HeuristicPlacer : 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 HeuristicMethod : uint8_t { + /// A-star algorithm + AStar, + /// Iterative diving search + IDS + }; + /// The heuristic method used for the search (default: IDS). + HeuristicMethod heuristicMethod = HeuristicMethod::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 +125,16 @@ class HeuristicPlacer : 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 = + heuristicMethod == HeuristicMethod::IDS ? 0.01F : 0.8F; /** * @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 = + heuristicMethod == HeuristicMethod::IDS ? 0.0F : 0.2F; /** * @brief The cost function can consider the distance of atoms to their * interaction partner in the next layer. @@ -131,7 +143,8 @@ class HeuristicPlacer : 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 = + heuristicMethod == HeuristicMethod::IDS ? 0.4F : 0.2F; /** * @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 +157,27 @@ class HeuristicPlacer : 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. In the current implementation, one node roughly consumes 140 + * Byte. Hence, allowing 10,000,000 nodes results in memory consumption of + * about 2 GB plus the size of the rest of the data structures. + */ + 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 maxNodes = 50'000'000; + 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; NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, useWindow, windowMinWidth, windowRatio, windowShare, deepeningFactor, deepeningValue, lookaheadFactor, - reuseLevel, maxNodes); + reuseLevel, maxNodes, trials, + queueCapacity); }; private: @@ -223,11 +247,13 @@ class HeuristicPlacer : 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 +285,13 @@ class HeuristicPlacer : 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 @@ -306,6 +334,261 @@ class HeuristicPlacer : 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; + }; + /// 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_; + + /** + * 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 (PriorityCompare{}(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 (PriorityCompare{}(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() && + PriorityCompare{}(minHeap_[leftChild]->value, + minHeap_[smallest]->value)) { + smallest = leftChild; + } + if (rightChild < minHeap_.size() && + PriorityCompare{}(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() && + PriorityCompare{}(maxHeap_[largest]->value, + maxHeap_[leftChild]->value)) { + largest = leftChild; + } + if (rightChild < maxHeap_.size() && + PriorityCompare{}(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: + explicit BoundedPriorityQueue(const SizeType maxQueueSize) + : heapCapacity_(maxQueueSize) { + minHeap_.reserve(heapCapacity_); + maxHeap_.reserve(heapCapacity_); + } + /** + * @returns the top element of the stack. + * @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 stack. + * @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; + heapifyMaxHeapDown(i); + } + } + assert(minHeap_.size() == maxHeap_.size()); + assert(minHeap_.size() <= heapCapacity_); + } + /// @returns `true` if the stack is empty. + [[nodiscard]] auto empty() const -> bool { return minHeap_.empty(); } + /// @brief Inserts an element at the top. + 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 (PriorityCompare{}(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. + * @param getHeuristic is a function returning the heuristic value of a given + * node. + * @param trials is the number of restarts IDS performs. + * @param queueCapacity is the capacity of the queue used for the iterative + * diving search. Must be larger or equal to @p trials. + * @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, + size_t queueCapacity) -> std::shared_ptr; + /** * @brief A* search algorithm for trees * @details A* is a graph traversal and path search algorithm that finds the @@ -322,11 +605,11 @@ class HeuristicPlacer : 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 @@ -363,7 +646,7 @@ class HeuristicPlacer : 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 +866,14 @@ class HeuristicPlacer : 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 +888,21 @@ class HeuristicPlacer : 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 From cc9b214e8a9e31fb7d5985f7d41b795972e4e29e Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:25:36 +0100 Subject: [PATCH 037/119] =?UTF-8?q?=F0=9F=8E=A8=20Update=20HeuristicPlacer?= =?UTF-8?q?.cpp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placer/HeuristicPlacer.hpp | 21 +- .../placer/HeuristicPlacer.cpp | 525 ++++++++++-------- 2 files changed, 310 insertions(+), 236 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 8a9c05554..10276a435 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -576,7 +576,8 @@ class HeuristicPlacer : public PlacerBase { * node. * @param trials is the number of restarts IDS performs. * @param queueCapacity is the capacity of the queue used for the iterative - * diving search. Must be larger or equal to @p trials. + * 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 @@ -626,18 +627,20 @@ class HeuristicPlacer : public PlacerBase { * particular node from the start node * @param getHeuristic is a function that returns the heuristic cost from the * node to any goal. + * @param maxNodes is the maximum number of held in the priority queue before + * the search is aborted. * @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; /** * @brief This function takes a list of atoms together with their current diff --git a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp index b70150ac7..d921a78ae 100644 --- a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp +++ b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -30,7 +29,7 @@ #include #include #include -#include +#include #include #include #include @@ -40,80 +39,139 @@ namespace na::zoned { template +auto HeuristicPlacer::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 { + 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; + } + }; + 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, restart search with priority {}", + goal->priority); + 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; +} +template auto HeuristicPlacer::aStarTreeSearch( - const Node& start, - const std::function>( - const Node&)>& getNeighbors, + std::shared_ptr start, + const std::function>( + std::shared_ptr)>& getNeighbors, const std::function& isGoal, const std::function& getCost, - const std::function& getHeuristic, - const size_t maxNodes) -> std::vector> { + const std::function& getHeuristic, size_t maxNodes) + -> std::shared_ptr { //===--------------------------------------------------------------------===// // 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_; + double priority; //< sum of cost and heuristic + std::shared_ptr node; //< pointer to the node - Item(const double priority, const Node& node, Item* parent) - : priority_(priority), node_(&node), parent_(parent) { + 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 { + 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_; + 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()); + // priority. + std::priority_queue, ItemCompare> openSet; + openSet.emplace(getHeuristic(*start), start); //===--------------------------------------------------------------------===// // Perform A* search //===--------------------------------------------------------------------===// - while (items.size() < maxNodes && !openSet.empty()) { - Item* itm = openSet.top(); + while (openSet.size() < maxNodes && !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_)) { - // 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; + 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_); + 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()); + const auto cost = getCost(*neighbor); + const auto heuristic = getHeuristic(*neighbor); + openSet.emplace(cost + heuristic, neighbor); } } } - if (items.size() >= maxNodes) { + 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 " @@ -140,12 +198,12 @@ auto HeuristicPlacer::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); } @@ -153,7 +211,7 @@ auto HeuristicPlacer::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); } @@ -172,7 +230,7 @@ auto HeuristicPlacer::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}); @@ -183,7 +241,7 @@ auto HeuristicPlacer::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}); @@ -194,12 +252,12 @@ auto HeuristicPlacer::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}; @@ -215,11 +273,11 @@ auto HeuristicPlacer::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; } } @@ -227,12 +285,12 @@ auto HeuristicPlacer::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; } } @@ -241,7 +299,7 @@ auto HeuristicPlacer::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); } @@ -249,7 +307,7 @@ auto HeuristicPlacer::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); } @@ -295,6 +353,7 @@ auto HeuristicPlacer::makeIntermediatePlacement( const auto& gatePlacement = placeGatesInEntanglementZone( previousPlacement, previousReuseQubits, twoQubitGates, reuseQubits, nextTwoQubitGates); + SPDLOG_TRACE("Placed gates"); return {gatePlacement, placeAtomsInStorageZone(gatePlacement, reuseQubits, twoQubitGates, nextTwoQubitGates)}; @@ -369,10 +428,10 @@ auto HeuristicPlacer::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]; @@ -427,7 +486,7 @@ auto HeuristicPlacer::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); @@ -486,7 +545,7 @@ auto HeuristicPlacer::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]; @@ -519,8 +578,7 @@ auto HeuristicPlacer::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); @@ -531,17 +589,17 @@ auto HeuristicPlacer::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)); @@ -557,8 +615,8 @@ auto HeuristicPlacer::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); @@ -569,8 +627,8 @@ auto HeuristicPlacer::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); @@ -580,8 +638,8 @@ auto HeuristicPlacer::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); @@ -592,8 +650,8 @@ auto HeuristicPlacer::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); @@ -605,19 +663,19 @@ auto HeuristicPlacer::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) { @@ -662,39 +720,34 @@ auto HeuristicPlacer::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) / @@ -702,42 +755,52 @@ auto HeuristicPlacer::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_.heuristicMethod) { + case Config::HeuristicMethod::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::HeuristicMethod::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); + } //===------------------------------------------------------------------===// // 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; } @@ -789,9 +852,9 @@ auto HeuristicPlacer::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] = @@ -871,7 +934,7 @@ auto HeuristicPlacer::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}); @@ -894,8 +957,7 @@ auto HeuristicPlacer::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{ @@ -910,19 +972,19 @@ auto HeuristicPlacer::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; @@ -940,8 +1002,8 @@ auto HeuristicPlacer::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, @@ -959,8 +1021,7 @@ auto HeuristicPlacer::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, @@ -976,8 +1037,8 @@ auto HeuristicPlacer::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, @@ -995,8 +1056,7 @@ auto HeuristicPlacer::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, @@ -1015,8 +1075,8 @@ auto HeuristicPlacer::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; }); @@ -1065,39 +1125,34 @@ auto HeuristicPlacer::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)), @@ -1108,41 +1163,58 @@ auto HeuristicPlacer::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_.heuristicMethod) { + case Config::HeuristicMethod::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::HeuristicMethod::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); + } //===------------------------------------------------------------------===// // 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; } @@ -1209,8 +1281,7 @@ auto HeuristicPlacer::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. @@ -1252,15 +1323,13 @@ auto HeuristicPlacer::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 } } @@ -1281,24 +1350,24 @@ auto HeuristicPlacer::getHeuristic(const std::vector& gateJobs, return heuristic; } -auto HeuristicPlacer::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 @@ -1312,18 +1381,18 @@ auto HeuristicPlacer::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 HeuristicPlacer::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; @@ -1333,14 +1402,14 @@ auto HeuristicPlacer::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); @@ -1357,7 +1426,7 @@ auto HeuristicPlacer::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; } @@ -1371,7 +1440,8 @@ auto HeuristicPlacer::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 { @@ -1380,13 +1450,13 @@ auto HeuristicPlacer::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}; } } @@ -1396,7 +1466,7 @@ auto HeuristicPlacer::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}; } } @@ -1418,7 +1488,7 @@ auto HeuristicPlacer::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); } @@ -1431,7 +1501,7 @@ auto HeuristicPlacer::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); @@ -1449,7 +1519,7 @@ HeuristicPlacer::HeuristicPlacer(const Architecture& architecture, // 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; @@ -1477,6 +1547,7 @@ auto HeuristicPlacer::place( : twoQubitGateLayers[layer + 1]); placement.emplace_back(gatePlacement); placement.emplace_back(qubitPlacement); + SPDLOG_DEBUG("Placed layer: {}", layer); } return placement; } From 013801f01ab05ecf1a1bbaf689c202d0a4b60e97 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:42:21 +0100 Subject: [PATCH 038/119] =?UTF-8?q?=F0=9F=93=9D=20Refine=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/RelaxedIndependentSetRouter.hpp | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp index 9068191cb..df5a81d01 100644 --- a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp @@ -125,22 +125,48 @@ class RelaxedIndependentSetRouter : public RouterBase { const std::tuple& w) -> bool; /** - * This struct indicates whether two movements are strictly compatible, + * @brief This struct indicates whether two movements are strictly compatible, * relaxed compatible together with the corresponding merging cost, or * (completely) incompatible. */ struct MovementCompatibility { enum class Status : uint8_t { - StrictlyCompatible, // Movements can proceed in parallel - RelaxedCompatible, // Can be merged with a cost - Incompatible // Cannot be merged at all + /** + * @brief The movements are strictly compatible. + * @details The atoms remain on the same row (column) and maintain their + * relative/topological order during the movement. + */ + StrictlyCompatible, + /** + * @brief The movements are compatible with respect to the relaxed routing + * constraints. + * @details The moved atoms must still remain on the same row (column) but + * may change their relative/topological order during the movement + * achieved by offsets during pick-up and drop-off. + */ + RelaxedCompatible, + /** + * @brief The movements are incompatible. + * @details The atoms starting in one row (column) do not end up in the + * same row (column). + */ + Incompatible }; /// Indicates the type of compatibility Status status; /** - * In the case of `RelaxedIncompatible`, the cost to merge the two - * movements + * @brief In the case of `RelaxedCompatible`, the cost to merge the two + * movements. + * @details The cost is calculated such that it represents the extra time + * the offset takes to shift the loaded atoms to deal with the relaxed + * routing constraints. More precisely, the cost is proportional to the + * cubed time for the sake of easier computation. If only one offset is + * required, i.e., either horizontal or vertical, the cost is the raw + * distance of the offset. If both offsets are required, then the cost is + * calculated as the sum of the third roots of the individual distances and + * then cubed again. + * Hence, the cost must always be a non-negative number. */ std::optional mergeCost; @@ -166,8 +192,7 @@ class RelaxedIndependentSetRouter : public RouterBase { * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) * @param w is the other 4D-vector of the form (x-start, y-start, x-end, * y-end) - * @return true, if the given movement vectors are incompatible, otherwise - * false + * @returns a @ref MovementCompatibility object indicating the compatibility. */ [[nodiscard]] static auto isRelaxedCompatibleMovement( const std::tuple& v, From b80464fe77fbef84355999626c0c2eacdc6d7177 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:45:53 +0100 Subject: [PATCH 039/119] =?UTF-8?q?=F0=9F=93=9D=20Correct=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/router/RelaxedIndependentSetRouter.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp index df5a81d01..c6c926960 100644 --- a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp @@ -161,7 +161,8 @@ class RelaxedIndependentSetRouter : public RouterBase { * @details The cost is calculated such that it represents the extra time * the offset takes to shift the loaded atoms to deal with the relaxed * routing constraints. More precisely, the cost is proportional to the - * cubed time for the sake of easier computation. If only one offset is + * cubed time for the sake of easier computation because then it is just + * proportional to the distance of the offset. If only one offset is * required, i.e., either horizontal or vertical, the cost is the raw * distance of the offset. If both offsets are required, then the cost is * calculated as the sum of the third roots of the individual distances and From 4b440b86f97f277217367c3e1a32b01c56d065d4 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:51:43 +0100 Subject: [PATCH 040/119] =?UTF-8?q?=F0=9F=93=9D=20Fix=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/RelaxedIndependentSetRouter.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp index a39774750..3aa1c7797 100644 --- a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp @@ -247,7 +247,8 @@ auto RelaxedIndependentSetRouter::route( const auto& independentSet = groupIt->independentSet; std::unordered_map atomToNewGroup; - // find best new group for each qubit in independent set and record costs + // find the best new group for each qubit in independent set and record + // costs auto totalCost = 0.0; auto totalCostCubed = 0.0; bool foundNewGroupForAllAtoms = true; @@ -313,15 +314,17 @@ auto RelaxedIndependentSetRouter::route( 3 * cost * totalCost * (cost + totalCost); totalCost += cost; } - // if all atoms in independent set could be assigned to a new group and - // the offset cost is less than the cost for the current group which is - // proportional to the cubed maximum distance + // if all atoms in the independent set could be assigned to a new group + // and the offset cost, i.e., the time for the extra offsets, is less than + // the cost for the current group. The cost for the current group is the + // cubic root of the distance; hence, we compare the cubes of the costs, + // i.e., the distance and the cubed costs directly. if (foundNewGroupForAllAtoms && groupIt->maxDistance > config_.preferSplit * totalCostCubed) { std::ranges::for_each(atomToNewGroup, [&relaxedConflictGraph, &atomToDist](const auto& pair) { const auto& [atom, group] = pair; - // add atom to new group + // add atom to a new group group->independentSet.emplace_back(atom); const auto dist = atomToDist.at(atom); if (group->maxDistance < dist) { From 98d1bd07c687f75e7fb36da2663afd77662e68ef Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:11:27 +0100 Subject: [PATCH 041/119] =?UTF-8?q?=F0=9F=8E=A8=20Combine=20both=20routers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/na/zoned/Compiler.hpp | 4 +- .../router/IndependentSetRouter.hpp | 133 ++++++- .../router/RelaxedIndependentSetRouter.hpp | 203 ---------- .../router/IndependentSetRouter.cpp | 305 ++++++++++++-- .../router/RelaxedIndependentSetRouter.cpp | 371 ------------------ 5 files changed, 400 insertions(+), 616 deletions(-) delete mode 100644 include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp delete mode 100644 src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp diff --git a/include/na/zoned/Compiler.hpp b/include/na/zoned/Compiler.hpp index 6397b052c..14b261f54 100644 --- a/include/na/zoned/Compiler.hpp +++ b/include/na/zoned/Compiler.hpp @@ -18,7 +18,6 @@ #include "layout_synthesizer/placer/AStarPlacer.hpp" #include "layout_synthesizer/placer/VertexMatchingPlacer.hpp" #include "layout_synthesizer/router/IndependentSetRouter.hpp" -#include "layout_synthesizer/router/RelaxedIndependentSetRouter.hpp" #include "na/NAComputation.hpp" #include "reuse_analyzer/VertexMatchingReuseAnalyzer.hpp" #include "scheduler/ASAPScheduler.hpp" @@ -292,8 +291,7 @@ class RoutingAwareCompiler final class RelaxedRoutingAwareSynthesizer : public PlaceAndRouteSynthesizer { + AStarPlacer, IndependentSetRouter> { public: RelaxedRoutingAwareSynthesizer(const Architecture& architecture, const Config& config) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 00a3b1c6d..415b4fe1d 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -33,21 +33,44 @@ class IndependentSetRouter : public RouterBase { public: /** - * The configuration of the IndependentSetRouter - * @note IndependentSetRouter does not have any configuration parameters. + * The configuration of the RelaxedIndependentSetRouter + * @note RelaxedIndependentSetRouter does not have any configuration + * parameters. */ struct Config { - template - friend void to_json(BasicJsonType& /* unused */, - const Config& /* unused */) {} - template - friend void from_json(const BasicJsonType& /* unused */, - Config& /* unused */) {} + enum class Method : uint8_t { + /** + * Use strict routing, i.e., the relative order of atoms must be + * maintained throughout a movement. + */ + STRICT, + /** + * Use relaxed routing, i.e., the relative order of atoms may change + * throughout a movement by applying offsets during pick-up and drop-off. + */ + RELAXED + }; + Method method = Method::STRICT; + /** + * @brief Threshold factor for group merging decisions during routing. + * @details First, a strict routing is computed resulting in a set of + * rearrangement groups. Afterward, some of those are merged with existing + * groups based on the relaxed constraints. Higher values of this + * parameter favor keeping groups separate; lower values favor merging. + * In particular, a value of 0.0 merges all possible groups. (Default: 1.0) + */ + double preferSplit = 1.0; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, method, preferSplit); }; - /// Create a IndependentSetRouter - IndependentSetRouter(const Architecture& architecture, - const Config& /* unused */) - : architecture_(architecture) {} + +private: + /// The configuration of the relaxed independent set router + Config config_; + +public: + /// Create a RelaxedIndependentSetRouter + IndependentSetRouter(const Architecture& architecture, const Config& config) + : architecture_(architecture), config_(config) {} /** * Given the computed placement, compute a possible routing. * @details For this task, all movements are put in a conflict graph where an @@ -81,6 +104,12 @@ class IndependentSetRouter : public RouterBase { const Placement& startPlacement, const Placement& targetPlacement) const -> std::unordered_map>; + [[nodiscard]] auto + createRelaxedConflictGraph(const std::vector& atomsToMove, + const Placement& startPlacement, + const Placement& targetPlacement) const + -> std::unordered_map< + qc::Qubit, std::vector>>>; /** * Takes two sites, the start and target site and returns a 4D-vector of the @@ -104,7 +133,83 @@ class IndependentSetRouter : public RouterBase { * @return true, if the given movement vectors are compatible, otherwise false */ [[nodiscard]] static auto - isCompatibleMovement(std::tuple v, - std::tuple w) -> bool; + isCompatibleMovement(const std::tuple& v, + const std::tuple& w) + -> bool; + /** + * @brief This struct indicates whether two movements are strictly compatible, + * relaxed compatible together with the corresponding merging cost, or + * (completely) incompatible. + */ + struct MovementCompatibility { + enum class Status : uint8_t { + /** + * @brief The movements are strictly compatible. + * @details The atoms remain on the same row (column) and maintain their + * relative/topological order during the movement. + */ + StrictlyCompatible, + /** + * @brief The movements are compatible with respect to the relaxed routing + * constraints. + * @details The moved atoms must still remain on the same row (column) but + * may change their relative/topological order during the movement + * achieved by offsets during pick-up and drop-off. + */ + RelaxedCompatible, + /** + * @brief The movements are incompatible. + * @details The atoms starting in one row (column) do not end up in the + * same row (column). + */ + Incompatible + }; + + /// Indicates the type of compatibility + Status status; + /** + * @brief In the case of `RelaxedCompatible`, the cost to merge the two + * movements. + * @details The cost is calculated such that it represents the extra time + * the offset takes to shift the loaded atoms to deal with the relaxed + * routing constraints. More precisely, the cost is proportional to the + * cubed time for the sake of easier computation because then it is just + * proportional to the distance of the offset. If only one offset is + * required, i.e., either horizontal or vertical, the cost is the raw + * distance of the offset. If both offsets are required, then the cost is + * calculated as the sum of the third roots of the individual distances and + * then cubed again. + * Hence, the cost must always be a non-negative number. + */ + std::optional mergeCost; + + /// Factory methods for strict compatibility + [[nodiscard]] static auto strictlyCompatible() -> MovementCompatibility { + return {.status = Status::StrictlyCompatible, .mergeCost = std::nullopt}; + } + + /// Factory method for incompatibility + [[nodiscard]] static auto incompatible() -> MovementCompatibility { + return {.status = Status::Incompatible, .mergeCost = std::nullopt}; + } + + [[nodiscard]] static auto relaxedCompatible(const double cost) + -> MovementCompatibility { + return {.status = Status::RelaxedCompatible, .mergeCost = cost}; + } + }; + /** + * Check whether two movements are incompatible with respect to the relaxed + * routing constraints, i.e., moved atoms remain not on the same row (column). + * This is, however, independent of their topological order (i.e., relaxed). + * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) + * @param w is the other 4D-vector of the form (x-start, y-start, x-end, + * y-end) + * @returns a @ref MovementCompatibility object indicating the compatibility. + */ + [[nodiscard]] static auto isRelaxedCompatibleMovement( + const std::tuple& v, + const std::tuple& w) + -> MovementCompatibility; }; } // namespace na::zoned diff --git a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp deleted file mode 100644 index c6c926960..000000000 --- a/include/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM - * Copyright (c) 2025 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "ir/Definitions.hpp" -#include "na/zoned/Architecture.hpp" -#include "na/zoned/Types.hpp" -#include "na/zoned/layout_synthesizer/router/RouterBase.hpp" - -#include -#include -#include -#include -#include - -namespace na::zoned { - -/** - * This class implements the default Router for the zoned neutral atom compiler - * that forms groups of parallel movements by calculating a maximal independent - * set. - */ -class RelaxedIndependentSetRouter : public RouterBase { - std::reference_wrapper architecture_; - -public: - /** - * The configuration of the RelaxedIndependentSetRouter - * @note RelaxedIndependentSetRouter does not have any configuration - * parameters. - */ - struct Config { - /** - * @brief Threshold factor for group merging decisions during routing. - * @details First, a strict routing is computed resulting in a set of - * rearrangement groups. Afterward, some of those are merged with existing - * groups based on the relaxed constraints. Higher values of this - * parameter favor keeping groups separate; lower values favor merging. - * In particular, a value of 0.0 merges all possible groups. (Default: 1.0) - */ - double preferSplit = 1.0; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, preferSplit); - }; - -private: - /// The configuration of the relaxed independent set router - Config config_; - -public: - /// Create a RelaxedIndependentSetRouter - RelaxedIndependentSetRouter(const Architecture& architecture, - const Config& config) - : architecture_(architecture), config_(config) {} - /** - * Given the computed placement, compute a possible routing. - * @details For this task, all movements are put in a conflict graph where an - * edge indicates that two atoms (nodes) cannot be moved together. The atoms - * are sorted by their distance in decreasing order such that atoms with - * larger distance are routed first and hopefully lead to more homogenous - * routing groups with similar movement distances within one group. - * @param placement is a vector of the atoms' placement at every layer - * @return the routing, i.e., for every transition between two placements a - * vector of groups containing atoms that can be moved simultaneously - */ - [[nodiscard]] auto route(const std::vector& placement) const - -> std::vector override; - -private: - /** - * Creates the conflict graph. - * @details Atom/qubit indices are the nodes. Two nodes are connected if their - * corresponding move with respect to the given @p start- and @p - * targetPlacement stand in conflict with each other. The graph is - * represented as adjacency lists. - * @param atomsToMove are all atoms corresponding to nodes in the graph - * @param startPlacement is the start placement of all atoms as a mapping from - * atoms to their sites - * @param targetPlacement is the target placement of the atoms - * @return the conflict graph as an unordered_map, where the keys are the - * nodes and the values are vectors of their neighbors - */ - [[nodiscard]] auto - createConflictGraph(const std::vector& atomsToMove, - const Placement& startPlacement, - const Placement& targetPlacement) const - -> std::unordered_map>; - [[nodiscard]] auto - createRelaxedConflictGraph(const std::vector& atomsToMove, - const Placement& startPlacement, - const Placement& targetPlacement) const - -> std::unordered_map< - qc::Qubit, std::vector>>>; - - /** - * Takes two sites, the start and target site and returns a 4D-vector of the - * form (x-start, y-start, x-end, y-end) where the corresponding x- and - * y-coordinates are the coordinates of the exact location of the given sites. - * @param start is the start site - * @param target is the target site - * @return is the 4D-vector containing the exact site locations - */ - [[nodiscard]] auto - getMovementVector(const std::tuple& start, - const std::tuple& target) const - -> std::tuple; - - /** - * Check whether two movements are compatible, i.e., the topological order - * of the moved atoms remain the same. - * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) - * @param w is the other 4D-vector of the form (x-start, y-start, x-end, - * y-end) - * @return true, if the given movement vectors are compatible, otherwise false - */ - [[nodiscard]] static auto - isCompatibleMovement(const std::tuple& v, - const std::tuple& w) - -> bool; - /** - * @brief This struct indicates whether two movements are strictly compatible, - * relaxed compatible together with the corresponding merging cost, or - * (completely) incompatible. - */ - struct MovementCompatibility { - enum class Status : uint8_t { - /** - * @brief The movements are strictly compatible. - * @details The atoms remain on the same row (column) and maintain their - * relative/topological order during the movement. - */ - StrictlyCompatible, - /** - * @brief The movements are compatible with respect to the relaxed routing - * constraints. - * @details The moved atoms must still remain on the same row (column) but - * may change their relative/topological order during the movement - * achieved by offsets during pick-up and drop-off. - */ - RelaxedCompatible, - /** - * @brief The movements are incompatible. - * @details The atoms starting in one row (column) do not end up in the - * same row (column). - */ - Incompatible - }; - - /// Indicates the type of compatibility - Status status; - /** - * @brief In the case of `RelaxedCompatible`, the cost to merge the two - * movements. - * @details The cost is calculated such that it represents the extra time - * the offset takes to shift the loaded atoms to deal with the relaxed - * routing constraints. More precisely, the cost is proportional to the - * cubed time for the sake of easier computation because then it is just - * proportional to the distance of the offset. If only one offset is - * required, i.e., either horizontal or vertical, the cost is the raw - * distance of the offset. If both offsets are required, then the cost is - * calculated as the sum of the third roots of the individual distances and - * then cubed again. - * Hence, the cost must always be a non-negative number. - */ - std::optional mergeCost; - - /// Factory methods for strict compatibility - [[nodiscard]] static auto strictlyCompatible() -> MovementCompatibility { - return {.status = Status::StrictlyCompatible, .mergeCost = std::nullopt}; - } - - /// Factory method for incompatibility - [[nodiscard]] static auto incompatible() -> MovementCompatibility { - return {.status = Status::Incompatible, .mergeCost = std::nullopt}; - } - - [[nodiscard]] static auto relaxedCompatible(const double cost) - -> MovementCompatibility { - return {.status = Status::RelaxedCompatible, .mergeCost = cost}; - } - }; - /** - * Check whether two movements are incompatible with respect to the relaxed - * routing constraints, i.e., moved atoms remain not on the same row (column). - * This is, however, independent of their topological order (i.e., relaxed). - * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) - * @param w is the other 4D-vector of the form (x-start, y-start, x-end, - * y-end) - * @returns a @ref MovementCompatibility object indicating the compatibility. - */ - [[nodiscard]] static auto isRelaxedCompatibleMovement( - const std::tuple& v, - const std::tuple& w) - -> MovementCompatibility; -}; -} // namespace na::zoned diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index 772de6bd6..c1e43b2d5 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -16,9 +16,8 @@ #include #include #include -#include +#include #include -#include #include #include #include @@ -49,6 +48,36 @@ auto IndependentSetRouter::createConflictGraph( } return conflictGraph; } +auto IndependentSetRouter::createRelaxedConflictGraph( + const std::vector& atomsToMove, const Placement& startPlacement, + const Placement& targetPlacement) const + -> std::unordered_map< + qc::Qubit, std::vector>>> { + std::unordered_map>>> + conflictGraph; + for (auto atomIt = atomsToMove.cbegin(); atomIt != atomsToMove.cend(); + ++atomIt) { + const auto& atom = *atomIt; + const auto& atomMovementVector = + getMovementVector(startPlacement[atom], targetPlacement[atom]); + for (auto neighborIt = atomIt + 1; neighborIt != atomsToMove.cend(); + ++neighborIt) { + const auto& neighbor = *neighborIt; + const auto& neighborMovementVector = getMovementVector( + startPlacement[neighbor], targetPlacement[neighbor]); + if (const auto& comp = isRelaxedCompatibleMovement( + atomMovementVector, neighborMovementVector); + comp.status != MovementCompatibility::Status::StrictlyCompatible) { + conflictGraph.try_emplace(atom).first->second.emplace_back( + neighbor, comp.mergeCost); + conflictGraph.try_emplace(neighbor).first->second.emplace_back( + atom, comp.mergeCost); + } + } + } + return conflictGraph; +} auto IndependentSetRouter::getMovementVector( const std::tuple& start, const std::tuple& target) const @@ -62,8 +91,8 @@ auto IndependentSetRouter::getMovementVector( return std::make_tuple(startX, startY, targetX, targetY); } auto IndependentSetRouter::isCompatibleMovement( - std::tuple v, - std::tuple w) -> bool { + const std::tuple& v, + const std::tuple& w) -> bool { const auto& [v0, v1, v2, v3] = v; const auto& [w0, w1, w2, w3] = w; if ((v0 == w0) != (v2 == w2)) { @@ -80,6 +109,50 @@ auto IndependentSetRouter::isCompatibleMovement( } return true; } +namespace { +auto sumCubeRootsCubed(const double a, const double b) -> double { + double x = std::cbrt(a); + double y = std::cbrt(b); + // (x+y)^3 = a + b + 3*x*y*(x+y) + return a + b + 3.0 * x * y * (x + y); +} +auto subCubeRootsCubed(const double a, const double b) -> double { + double x = std::cbrt(a); + double y = std::cbrt(b); + // (x-y)^3 = a - b + 3*x*y*(y-x) + return a - b + 3.0 * x * y * (y - x); +} +} // namespace +auto IndependentSetRouter::isRelaxedCompatibleMovement( + const std::tuple& v, + const std::tuple& w) + -> MovementCompatibility { + const auto& [v0, v1, v2, v3] = v; + const auto& [w0, w1, w2, w3] = w; + if (((v0 == w0) != (v2 == w2)) || ((v1 == w1) != (v3 == w3))) { + return MovementCompatibility::incompatible(); + } + if ((v0 < w0) != (v2 < w2) && (v1 < w1) != (v3 < w3)) { + return MovementCompatibility::relaxedCompatible(sumCubeRootsCubed( + static_cast( + std::abs(static_cast(v0) - static_cast(w0)) + + std::abs(static_cast(v2) - static_cast(w2))), + static_cast( + std::abs(static_cast(v1) - static_cast(w1)) + + std::abs(static_cast(v3) - static_cast(w3))))); + } + if ((v0 < w0) != (v2 < w2)) { + return MovementCompatibility::relaxedCompatible(static_cast( + std::abs(static_cast(v0) - static_cast(w0)) + + std::abs(static_cast(v2) - static_cast(w2)))); + } + if ((v1 < w1) != (v3 < w3)) { + return MovementCompatibility::relaxedCompatible(static_cast( + std::abs(static_cast(v1) - static_cast(w1)) + + std::abs(static_cast(v3) - static_cast(w3)))); + } + return MovementCompatibility::strictlyCompatible(); +} auto IndependentSetRouter::route(const std::vector& placement) const -> std::vector { std::vector routing; @@ -92,6 +165,7 @@ auto IndependentSetRouter::route(const std::vector& placement) const const auto& targetPlacement = placement[i + 1]; std::set, std::greater<>> atomsToMoveOrderedAscByDist; + std::unordered_map atomToDist; assert(startPlacement.size() == targetPlacement.size()); for (qc::Qubit atom = 0; atom < startPlacement.size(); ++atom) { const auto& [startSLM, startRow, startColumn] = startPlacement[atom]; @@ -103,40 +177,221 @@ auto IndependentSetRouter::route(const std::vector& placement) const architecture_.get().distance(startSLM, startRow, startColumn, targetSLM, targetRow, targetColumn); atomsToMoveOrderedAscByDist.emplace(distance, atom); + atomToDist.emplace(atom, distance); } } std::vector atomsToMove; atomsToMove.reserve(atomsToMoveOrderedAscByDist.size()); // put the atoms into the vector such they are ordered decreasingly by their // movement distance - for (const auto& atomIt : atomsToMoveOrderedAscByDist) { - atomsToMove.emplace_back(atomIt.second); + for (const auto& val : atomsToMoveOrderedAscByDist | std::views::values) { + atomsToMove.emplace_back(val); } - auto conflictGraph = + const auto conflictGraph = createConflictGraph(atomsToMove, startPlacement, targetPlacement); - auto& currentRouting = routing.emplace_back(); - while (!atomsToMove.empty()) { - auto& independentSet = currentRouting.emplace_back(); - std::vector remainingAtoms; - std::unordered_set conflictingAtomsMoves; - for (const auto& atom : atomsToMove) { - if (conflictingAtomsMoves.find(atom) == conflictingAtomsMoves.end()) { - // if the atom does not conflict with any atom that is already in the - // independent set, add it and mark its neighbors as conflicting - independentSet.emplace_back(atom); - if (const auto conflictingNeighbors = conflictGraph.find(atom); - conflictingNeighbors != conflictGraph.end()) { - for (const auto neighbor : conflictingNeighbors->second) { - conflictingAtomsMoves.emplace(neighbor); + if (config_.method == Config::Method::STRICT) { + struct GroupInfo { + std::vector independentSet; + }; + std::list groups; + while (!atomsToMove.empty()) { + auto& group = groups.emplace_back(); + std::vector remainingAtoms; + std::unordered_set conflictingAtoms; + for (const auto& atom : atomsToMove) { + if (!conflictingAtoms.contains(atom)) { + // if the atom does not conflict with any atom that is already in + // the independent set, add it and mark its neighbors as conflicting + group.independentSet.emplace_back(atom); + if (const auto conflictingNeighbors = conflictGraph.find(atom); + conflictingNeighbors != conflictGraph.end()) { + for (const auto neighbor : conflictingNeighbors->second) { + conflictingAtoms.emplace(neighbor); + } } + } else { + // if an atom could not be put into the current independent set, add + // it to the remaining atoms + remainingAtoms.emplace_back(atom); } + } + atomsToMove = remainingAtoms; + } + } else { + const auto relaxedConflictGraph = createRelaxedConflictGraph( + atomsToMove, startPlacement, targetPlacement); + struct GroupInfo { + std::vector independentSet; + double maxDistance = 0.0; + std::unordered_map> + relaxedConflictingAtoms; + }; + std::list groups; + while (!atomsToMove.empty()) { + auto& group = groups.emplace_back(); + std::vector remainingAtoms; + std::unordered_set conflictingAtoms; + for (const auto& atom : atomsToMove) { + if (!conflictingAtoms.contains(atom)) { + // if the atom does not conflict with any atom that is already in + // the independent set, add it and mark its neighbors as conflicting + group.independentSet.emplace_back(atom); + const auto dist = atomToDist.at(atom); + if (group.maxDistance < dist) { + group.maxDistance = dist; + } + if (const auto conflictingNeighbors = conflictGraph.find(atom); + conflictingNeighbors != conflictGraph.end()) { + for (const auto neighbor : conflictingNeighbors->second) { + conflictingAtoms.emplace(neighbor); + } + assert(relaxedConflictGraph.contains(atom)); + for (const auto neighbor : relaxedConflictGraph.at(atom)) { + auto [conflictIt, success] = + group.relaxedConflictingAtoms.try_emplace(neighbor.first, + neighbor.second); + if (!success && conflictIt->second.has_value()) { + if (neighbor.second.has_value()) { + conflictIt->second = + std::max(*conflictIt->second, *neighbor.second); + } else { + conflictIt->second = std::nullopt; + } + } + } + } + } else { + // if an atom could not be put into the current independent set, add + // it to the remaining atoms + remainingAtoms.emplace_back(atom); + } + } + atomsToMove = remainingAtoms; + } + // try to merge rearrangement steps + for (auto groupIt = groups.rbegin(); groupIt != groups.rend();) { + const auto& independentSet = groupIt->independentSet; + std::unordered_map + atomToNewGroup; + // find the best new group for each qubit in independent set and record + // costs + auto totalCost = 0.0; + auto totalCostCubed = 0.0; + bool foundNewGroupForAllAtoms = true; + for (const auto& atom : independentSet) { + bool foundNewGroup = false; + auto cost = std::numeric_limits::max(); + auto costCubed = std::numeric_limits::max(); + for (auto& group : groups | std::views::reverse) { + // filter current group + if (&group != &*groupIt) { + if (const auto conflictIt = + group.relaxedConflictingAtoms.find(atom); + conflictIt == group.relaxedConflictingAtoms.end()) { + const auto dist = atomToDist.at(atom); + if (group.maxDistance >= dist) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + cost = 0; + costCubed = 0; + break; + } + const auto diff = subCubeRootsCubed(dist, group.maxDistance); + if (costCubed > diff) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = diff; + cost = std::cbrt(diff); + } + } else if (conflictIt->second.has_value()) { + // can be added with additional cost because there is a strict + // conflict + const auto dist = atomToDist.at(atom); + if (group.maxDistance > dist) { + if (costCubed > *conflictIt->second) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = *conflictIt->second; + cost = std::cbrt(*conflictIt->second); + } + } else { + const auto c = sumCubeRootsCubed( + subCubeRootsCubed(dist, group.maxDistance), + *conflictIt->second); + if (costCubed > c) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = c; + cost = std::cbrt(c); + } + } + } + } + } + if (!foundNewGroup) { + foundNewGroupForAllAtoms = false; + break; + } + // Note the following identity to calculate the new cubed offset as + // offsetCostCubed' = (offsetCost + bestCost)^3 + // + // Identity: (x + y)^3 = x^3 + y^3 + 3xy(x+y) + totalCostCubed = costCubed + totalCostCubed + + 3 * cost * totalCost * (cost + totalCost); + totalCost += cost; + } + // if all atoms in the independent set could be assigned to a new group + // and the offset cost, i.e., the time for the extra offsets, is less + // than the cost for the current group. The cost for the current group + // is the cubic root of the distance; hence, we compare the cubes of the + // costs, i.e., the distance and the cubed costs directly. + if (foundNewGroupForAllAtoms && + groupIt->maxDistance > config_.preferSplit * totalCostCubed) { + std::ranges::for_each(atomToNewGroup, [&relaxedConflictGraph, + &atomToDist]( + const auto& pair) { + const auto& [atom, group] = pair; + // add atom to a new group + group->independentSet.emplace_back(atom); + const auto dist = atomToDist.at(atom); + if (group->maxDistance < dist) { + group->maxDistance = dist; + } + if (const auto relaxedConflictingNeighbors = + relaxedConflictGraph.find(atom); + relaxedConflictingNeighbors != relaxedConflictGraph.end()) { + for (const auto neighbor : relaxedConflictingNeighbors->second) { + auto [conflictIt, success] = + group->relaxedConflictingAtoms.try_emplace(neighbor.first, + neighbor.second); + if (!success && conflictIt->second.has_value()) { + if (neighbor.second.has_value()) { + conflictIt->second = + std::max(*conflictIt->second, *neighbor.second); + } else { + conflictIt->second = std::nullopt; + } + } + } + } + }); + // erase the current group from the linked list of groups; note that + // a reverse pointer always points to the element in front of the + // current iterator position. + // After erasing, we create a new reverse iterator pointing to the + // same logical position in the remaining list. + const auto& a = (++groupIt).base(); + const auto& b = groups.erase(a); + groupIt = std::make_reverse_iterator(b); } else { - // if an atom could not be put into the current independent set, add - // it to the remaining atoms - remainingAtoms.emplace_back(atom); + ++groupIt; } } - atomsToMove = remainingAtoms; + auto& currentRouting = routing.emplace_back(); + currentRouting.reserve(groups.size()); + for (auto& group : groups) { + currentRouting.emplace_back(std::move(group.independentSet)); + } } } return routing; diff --git a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp deleted file mode 100644 index 3aa1c7797..000000000 --- a/src/na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.cpp +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM - * Copyright (c) 2025 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "na/zoned/layout_synthesizer/router/RelaxedIndependentSetRouter.hpp" - -#include "ir/Definitions.hpp" -#include "na/zoned/Architecture.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace na::zoned { -auto RelaxedIndependentSetRouter::createConflictGraph( - const std::vector& atomsToMove, const Placement& startPlacement, - const Placement& targetPlacement) const - -> std::unordered_map> { - std::unordered_map> conflictGraph; - for (auto atomIt = atomsToMove.cbegin(); atomIt != atomsToMove.cend(); - ++atomIt) { - const auto& atom = *atomIt; - const auto& atomMovementVector = - getMovementVector(startPlacement[atom], targetPlacement[atom]); - for (auto neighborIt = atomIt + 1; neighborIt != atomsToMove.cend(); - ++neighborIt) { - const auto& neighbor = *neighborIt; - const auto& neighborMovementVector = getMovementVector( - startPlacement[neighbor], targetPlacement[neighbor]); - if (!isCompatibleMovement(atomMovementVector, neighborMovementVector)) { - conflictGraph.try_emplace(atom).first->second.emplace_back(neighbor); - conflictGraph.try_emplace(neighbor).first->second.emplace_back(atom); - } - } - } - return conflictGraph; -} -auto RelaxedIndependentSetRouter::createRelaxedConflictGraph( - const std::vector& atomsToMove, const Placement& startPlacement, - const Placement& targetPlacement) const - -> std::unordered_map< - qc::Qubit, std::vector>>> { - std::unordered_map>>> - conflictGraph; - for (auto atomIt = atomsToMove.cbegin(); atomIt != atomsToMove.cend(); - ++atomIt) { - const auto& atom = *atomIt; - const auto& atomMovementVector = - getMovementVector(startPlacement[atom], targetPlacement[atom]); - for (auto neighborIt = atomIt + 1; neighborIt != atomsToMove.cend(); - ++neighborIt) { - const auto& neighbor = *neighborIt; - const auto& neighborMovementVector = getMovementVector( - startPlacement[neighbor], targetPlacement[neighbor]); - if (const auto& comp = isRelaxedCompatibleMovement( - atomMovementVector, neighborMovementVector); - comp.status != MovementCompatibility::Status::StrictlyCompatible) { - conflictGraph.try_emplace(atom).first->second.emplace_back( - neighbor, comp.mergeCost); - conflictGraph.try_emplace(neighbor).first->second.emplace_back( - atom, comp.mergeCost); - } - } - } - return conflictGraph; -} -auto RelaxedIndependentSetRouter::getMovementVector( - const std::tuple& start, - const std::tuple& target) const - -> std::tuple { - const auto& [startSLM, startRow, startColumn] = start; - const auto& [startX, startY] = - architecture_.get().exactSLMLocation(startSLM, startRow, startColumn); - const auto& [targetSLM, targetRow, targetColumn] = target; - const auto& [targetX, targetY] = - architecture_.get().exactSLMLocation(targetSLM, targetRow, targetColumn); - return std::make_tuple(startX, startY, targetX, targetY); -} -auto RelaxedIndependentSetRouter::isCompatibleMovement( - const std::tuple& v, - const std::tuple& w) -> bool { - const auto& [v0, v1, v2, v3] = v; - const auto& [w0, w1, w2, w3] = w; - if ((v0 == w0) != (v2 == w2)) { - return false; - } - if ((v0 < w0) != (v2 < w2)) { - return false; - } - if ((v1 == w1) != (v3 == w3)) { - return false; - } - if ((v1 < w1) != (v3 < w3)) { - return false; - } - return true; -} -namespace { -auto sumCubeRootsCubed(const double a, const double b) -> double { - double x = std::cbrt(a); - double y = std::cbrt(b); - // (x+y)^3 = a + b + 3*x*y*(x+y) - return a + b + 3.0 * x * y * (x + y); -} -auto subCubeRootsCubed(const double a, const double b) -> double { - double x = std::cbrt(a); - double y = std::cbrt(b); - // (x-y)^3 = a - b + 3*x*y*(y-x) - return a - b + 3.0 * x * y * (y - x); -} -} // namespace -auto RelaxedIndependentSetRouter::isRelaxedCompatibleMovement( - const std::tuple& v, - const std::tuple& w) - -> MovementCompatibility { - const auto& [v0, v1, v2, v3] = v; - const auto& [w0, w1, w2, w3] = w; - if (((v0 == w0) != (v2 == w2)) || ((v1 == w1) != (v3 == w3))) { - return MovementCompatibility::incompatible(); - } - if ((v0 < w0) != (v2 < w2) && (v1 < w1) != (v3 < w3)) { - return MovementCompatibility::relaxedCompatible(sumCubeRootsCubed( - static_cast( - std::abs(static_cast(v0) - static_cast(w0)) + - std::abs(static_cast(v2) - static_cast(w2))), - static_cast( - std::abs(static_cast(v1) - static_cast(w1)) + - std::abs(static_cast(v3) - static_cast(w3))))); - } - if ((v0 < w0) != (v2 < w2)) { - return MovementCompatibility::relaxedCompatible(static_cast( - std::abs(static_cast(v0) - static_cast(w0)) + - std::abs(static_cast(v2) - static_cast(w2)))); - } - if ((v1 < w1) != (v3 < w3)) { - return MovementCompatibility::relaxedCompatible(static_cast( - std::abs(static_cast(v1) - static_cast(w1)) + - std::abs(static_cast(v3) - static_cast(w3)))); - } - return MovementCompatibility::strictlyCompatible(); -} -auto RelaxedIndependentSetRouter::route( - const std::vector& placement) const -> std::vector { - std::vector routing; - // early return if no placement is given - if (placement.empty()) { - return routing; - } - for (auto it = placement.cbegin(); true;) { - const auto& startPlacement = *it; - if (++it == placement.cend()) { - break; - } - const auto& targetPlacement = *it; - std::set, std::greater<>> - atomsToMoveOrderedAscByDist; - std::unordered_map atomToDist; - assert(startPlacement.size() == targetPlacement.size()); - for (qc::Qubit atom = 0; atom < startPlacement.size(); ++atom) { - const auto& [startSLM, startRow, startColumn] = startPlacement[atom]; - const auto& [targetSLM, targetRow, targetColumn] = targetPlacement[atom]; - // if atom must be moved - if (&startSLM.get() != &targetSLM.get() || startRow != targetRow || - startColumn != targetColumn) { - const auto distance = - architecture_.get().distance(startSLM, startRow, startColumn, - targetSLM, targetRow, targetColumn); - atomsToMoveOrderedAscByDist.emplace(distance, atom); - atomToDist.emplace(atom, distance); - } - } - std::vector atomsToMove; - atomsToMove.reserve(atomsToMoveOrderedAscByDist.size()); - // put the atoms into the vector such they are ordered decreasingly by their - // movement distance - for (const auto& val : atomsToMoveOrderedAscByDist | std::views::values) { - atomsToMove.emplace_back(val); - } - const auto conflictGraph = - createConflictGraph(atomsToMove, startPlacement, targetPlacement); - const auto relaxedConflictGraph = createRelaxedConflictGraph( - atomsToMove, startPlacement, targetPlacement); - struct GroupInfo { - std::vector independentSet; - double maxDistance = 0.0; - std::unordered_map> - relaxedConflictingAtoms; - }; - std::list groups; - while (!atomsToMove.empty()) { - auto& group = groups.emplace_back(); - std::vector remainingAtoms; - std::unordered_set conflictingAtoms; - for (const auto& atom : atomsToMove) { - if (!conflictingAtoms.contains(atom)) { - // if the atom does not conflict with any atom that is already in the - // independent set, add it and mark its neighbors as conflicting - group.independentSet.emplace_back(atom); - const auto dist = atomToDist.at(atom); - if (group.maxDistance < dist) { - group.maxDistance = dist; - } - if (const auto conflictingNeighbors = conflictGraph.find(atom); - conflictingNeighbors != conflictGraph.end()) { - for (const auto neighbor : conflictingNeighbors->second) { - conflictingAtoms.emplace(neighbor); - } - assert(relaxedConflictGraph.contains(atom)); - for (const auto neighbor : relaxedConflictGraph.at(atom)) { - auto [conflictIt, success] = - group.relaxedConflictingAtoms.try_emplace(neighbor.first, - neighbor.second); - if (!success && conflictIt->second.has_value()) { - if (neighbor.second.has_value()) { - conflictIt->second = - std::max(*conflictIt->second, *neighbor.second); - } else { - conflictIt->second = std::nullopt; - } - } - } - } - } else { - // if an atom could not be put into the current independent set, add - // it to the remaining atoms - remainingAtoms.emplace_back(atom); - } - } - atomsToMove = remainingAtoms; - } - // try to merge rearrangement steps - for (auto groupIt = groups.rbegin(); groupIt != groups.rend();) { - const auto& independentSet = groupIt->independentSet; - std::unordered_map - atomToNewGroup; - // find the best new group for each qubit in independent set and record - // costs - auto totalCost = 0.0; - auto totalCostCubed = 0.0; - bool foundNewGroupForAllAtoms = true; - for (const auto& atom : independentSet) { - bool foundNewGroup = false; - auto cost = std::numeric_limits::max(); - auto costCubed = std::numeric_limits::max(); - for (auto& group : groups | std::views::reverse) { - // filter current group - if (&group != &*groupIt) { - if (const auto conflictIt = - group.relaxedConflictingAtoms.find(atom); - conflictIt == group.relaxedConflictingAtoms.end()) { - const auto dist = atomToDist.at(atom); - if (group.maxDistance >= dist) { - foundNewGroup = true; - atomToNewGroup.insert_or_assign(atom, &group); - cost = 0; - costCubed = 0; - break; - } - const auto diff = subCubeRootsCubed(dist, group.maxDistance); - if (costCubed > diff) { - foundNewGroup = true; - atomToNewGroup.insert_or_assign(atom, &group); - costCubed = diff; - cost = std::cbrt(diff); - } - } else if (conflictIt->second.has_value()) { - // can be added with additional cost because there is a strict - // conflict - const auto dist = atomToDist.at(atom); - if (group.maxDistance > dist) { - if (costCubed > *conflictIt->second) { - foundNewGroup = true; - atomToNewGroup.insert_or_assign(atom, &group); - costCubed = *conflictIt->second; - cost = std::cbrt(*conflictIt->second); - } - } else { - const auto c = sumCubeRootsCubed( - subCubeRootsCubed(dist, group.maxDistance), - *conflictIt->second); - if (costCubed > c) { - foundNewGroup = true; - atomToNewGroup.insert_or_assign(atom, &group); - costCubed = c; - cost = std::cbrt(c); - } - } - } - } - } - if (!foundNewGroup) { - foundNewGroupForAllAtoms = false; - break; - } - // Note the following identity to calculate the new cubed offset as - // offsetCostCubed' = (offsetCost + bestCost)^3 - // - // Identity: (x + y)^3 = x^3 + y^3 + 3xy(x+y) - totalCostCubed = costCubed + totalCostCubed + - 3 * cost * totalCost * (cost + totalCost); - totalCost += cost; - } - // if all atoms in the independent set could be assigned to a new group - // and the offset cost, i.e., the time for the extra offsets, is less than - // the cost for the current group. The cost for the current group is the - // cubic root of the distance; hence, we compare the cubes of the costs, - // i.e., the distance and the cubed costs directly. - if (foundNewGroupForAllAtoms && - groupIt->maxDistance > config_.preferSplit * totalCostCubed) { - std::ranges::for_each(atomToNewGroup, [&relaxedConflictGraph, - &atomToDist](const auto& pair) { - const auto& [atom, group] = pair; - // add atom to a new group - group->independentSet.emplace_back(atom); - const auto dist = atomToDist.at(atom); - if (group->maxDistance < dist) { - group->maxDistance = dist; - } - if (const auto relaxedConflictingNeighbors = - relaxedConflictGraph.find(atom); - relaxedConflictingNeighbors != relaxedConflictGraph.end()) { - for (const auto neighbor : relaxedConflictingNeighbors->second) { - auto [conflictIt, success] = - group->relaxedConflictingAtoms.try_emplace(neighbor.first, - neighbor.second); - if (!success && conflictIt->second.has_value()) { - if (neighbor.second.has_value()) { - conflictIt->second = - std::max(*conflictIt->second, *neighbor.second); - } else { - conflictIt->second = std::nullopt; - } - } - } - } - }); - // erase the current group from the linked list of groups; note that - // a reverse pointer always points to the element in front of the - // current iterator position. - // After erasing, we create a new reverse iterator pointing to the - // same logical position in the remaining list. - const auto& a = (++groupIt).base(); - const auto& b = groups.erase(a); - groupIt = std::make_reverse_iterator(b); - } else { - ++groupIt; - } - } - auto& currentRouting = routing.emplace_back(); - currentRouting.reserve(groups.size()); - for (auto& group : groups) { - currentRouting.emplace_back(std::move(group.independentSet)); - } - } - return routing; -} -} // namespace na::zoned From a9f018d6fd957b74fe989152d2710301a99ca3f9 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:24:09 +0100 Subject: [PATCH 042/119] =?UTF-8?q?=F0=9F=8E=A8=20Adapt=20test=20suite=20a?= =?UTF-8?q?nd=20fix=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/na/zoned/Compiler.hpp | 22 ------------ .../router/IndependentSetRouter.hpp | 3 +- .../router/IndependentSetRouter.cpp | 4 +-- test/na/zoned/test_compiler.cpp | 36 ++++++++++++------- test/na/zoned/test_independent_set_router.cpp | 3 +- 5 files changed, 29 insertions(+), 39 deletions(-) diff --git a/include/na/zoned/Compiler.hpp b/include/na/zoned/Compiler.hpp index 14b261f54..6546d22b8 100644 --- a/include/na/zoned/Compiler.hpp +++ b/include/na/zoned/Compiler.hpp @@ -288,26 +288,4 @@ class RoutingAwareCompiler final explicit RoutingAwareCompiler(const Architecture& architecture) : Compiler(architecture) {} }; - -class RelaxedRoutingAwareSynthesizer - : public PlaceAndRouteSynthesizer { -public: - RelaxedRoutingAwareSynthesizer(const Architecture& architecture, - const Config& config) - : PlaceAndRouteSynthesizer(architecture, config) {} - explicit RelaxedRoutingAwareSynthesizer(const Architecture& architecture) - : PlaceAndRouteSynthesizer(architecture) {} -}; -class RelaxedRoutingAwareCompiler final - : public Compiler { -public: - RelaxedRoutingAwareCompiler(const Architecture& architecture, - const Config& config) - : Compiler(architecture, config) {} - explicit RelaxedRoutingAwareCompiler(const Architecture& architecture) - : Compiler(architecture) {} -}; } // namespace na::zoned diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 415b4fe1d..f91e66636 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -50,7 +50,7 @@ class IndependentSetRouter : public RouterBase { */ RELAXED }; - Method method = Method::STRICT; + Method method = Method::RELAXED; /** * @brief Threshold factor for group merging decisions during routing. * @details First, a strict routing is computed resulting in a set of @@ -58,6 +58,7 @@ class IndependentSetRouter : public RouterBase { * groups based on the relaxed constraints. Higher values of this * parameter favor keeping groups separate; lower values favor merging. * In particular, a value of 0.0 merges all possible groups. (Default: 1.0) + * @note This value is only relevant if the routing method RELAXED is used. */ double preferSplit = 1.0; NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, method, preferSplit); diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index c1e43b2d5..ab122c0b6 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -304,8 +304,8 @@ auto IndependentSetRouter::route(const std::vector& placement) const cost = std::cbrt(diff); } } else if (conflictIt->second.has_value()) { - // can be added with additional cost because there is a strict - // conflict + // can be added with additional cost because there is only a + // relaxed conflict const auto dist = atomToDist.at(atom); if (group.maxDistance > dist) { if (costCubed > *conflictIt->second) { diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 37174a3c5..5b6e2ffb6 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -39,20 +39,23 @@ constexpr std::string_view architectureSpecification = R"({ "aods":[{"id": 0, "site_separation": 2, "r": 20, "c": 20}], "rydberg_range": [[[5, 70], [55, 110]]] })"; -constexpr std::string_view routingAgnosticConfiguration = R"({ +constexpr std::string_view strictRoutingAgnosticConfiguration = R"({ "logLevel" : 1, "layoutSynthesizerConfig" : { "placerConfig" : { "useWindow" : true, "windowSize" : 10, "dynamicPlacement" : true + }, + "routerConfig" : { + "method" : "STRICT" } }, "codeGeneratorConfig" : { "warnUnsupportedGates" : false } })"; -constexpr std::string_view routingAwareConfiguration = R"({ +constexpr std::string_view strictRoutingAwareConfiguration = R"({ "logLevel" : 1, "codeGeneratorConfig" : { "warnUnsupportedGates" : false @@ -67,6 +70,9 @@ constexpr std::string_view routingAwareConfiguration = R"({ "deepeningValue" : 0.2, "lookaheadFactor": 0.2, "reuseLevel": 5.0 + }, + "routerConfig" : { + "method" : "STRICT" } } })"; @@ -87,25 +93,26 @@ constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ "reuseLevel": 5.0 }, "routerConfig" : { + "method" : "RELAXED", "preferSplit" : 0.0 } } })"; -#define COMPILER_TEST(compiler_type, config) \ - TEST(compiler_type##Test, ConstructorWithoutConfig) { \ +#define COMPILER_TEST(test_name, compiler_type, config) \ + TEST(test_name##Test, ConstructorWithoutConfig) { \ Architecture architecture( \ Architecture::fromJSONString(architectureSpecification)); \ /* expected not to lead to a segfault */ \ [[maybe_unused]] compiler_type compiler(architecture); \ } \ - class compiler_type##Test : public ::testing::TestWithParam { \ + class test_name##Test : public ::testing::TestWithParam { \ compiler_type::Config config_; \ Architecture architecture_; \ \ protected: \ qc::QuantumComputation circ_; \ compiler_type compiler_; \ - compiler_type##Test() \ + test_name##Test() \ : config_(nlohmann::json::parse(config)), \ architecture_( \ Architecture::fromJSONString(architectureSpecification)), \ @@ -113,14 +120,14 @@ constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ void SetUp() override { circ_ = qasm3::Importer::importf(GetParam()); } \ }; \ /*=========================== END TO END TESTS ===========================*/ \ - TEST_P(compiler_type##Test, EndToEnd) { \ + TEST_P(test_name##Test, EndToEnd) { \ const auto& code = this->compiler_.compile(this->circ_); \ EXPECT_TRUE(code.validate().first); \ /*===----------------------------------------------------------------===*/ \ /* write code to a file with extension .naviz in a directory converted */ \ std::filesystem::path inputFile(GetParam()); \ std::filesystem::path outputFile = inputFile.parent_path() / "converted" / \ - #compiler_type / \ + #test_name / \ (inputFile.stem().string() + ".naviz"); \ std::filesystem::create_directories(outputFile.parent_path()); \ std::ofstream output(outputFile); \ @@ -137,17 +144,20 @@ constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ } \ /*========================================================================*/ \ INSTANTIATE_TEST_SUITE_P( \ - compiler_type##TestWithCircuits, /* Custom instantiation name */ \ - compiler_type##Test, /* Test suite name */ \ + test_name##TestWithCircuits, /* Custom instantiation name */ \ + test_name##Test, /* Test suite name */ \ ::testing::Values(TEST_CIRCUITS), /* Parameters to test with */ \ [](const ::testing::TestParamInfo& pinfo) { \ const std::filesystem::path path(pinfo.param); \ return path.stem().string(); \ }) /*============================== INSTANTIATIONS ==============================*/ -COMPILER_TEST(RoutingAgnosticCompiler, routingAgnosticConfiguration); -COMPILER_TEST(RoutingAwareCompiler, routingAwareConfiguration); -COMPILER_TEST(RelaxedRoutingAwareCompiler, relaxedRoutingAwareConfiguration); +COMPILER_TEST(StrictRoutingAgnosticCompiler, RoutingAgnosticCompiler, + strictRoutingAgnosticConfiguration); +COMPILER_TEST(StrictRoutingAwareCompiler, RoutingAwareCompiler, + strictRoutingAwareConfiguration); +COMPILER_TEST(RelaxedRoutingAwareCompiler, RoutingAwareCompiler, + relaxedRoutingAwareConfiguration); // Tests that the bug described in issue // https://github.com/munich-quantum-toolkit/qmap/issues/727 is fixed. diff --git a/test/na/zoned/test_independent_set_router.cpp b/test/na/zoned/test_independent_set_router.cpp index 97083a18a..ec0f2da5f 100644 --- a/test/na/zoned/test_independent_set_router.cpp +++ b/test/na/zoned/test_independent_set_router.cpp @@ -44,7 +44,8 @@ constexpr std::string_view architectureJson = R"({ class IndependentSetRouterRouteTest : public ::testing::Test { protected: Architecture architecture; - nlohmann::json config; + IndependentSetRouter::Config config{ + .method = IndependentSetRouter::Config::Method::STRICT}; IndependentSetRouter router; IndependentSetRouterRouteTest() : architecture(Architecture::fromJSONString(architectureJson)), From 309194a9daea4a00092487618c9eca6c4a576d5e Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:24:47 +0100 Subject: [PATCH 043/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoned/layout_synthesizer/router/IndependentSetRouter.hpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index f91e66636..c8e319637 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -24,9 +24,8 @@ namespace na::zoned { /** - * This class implements the default Router for the zoned neutral atom compiler - * that forms groups of parallel movements by calculating a maximal independent - * set. + * This class implements a Router for the zoned neutral atom compiler that forms + * groups of parallel movements by calculating a maximal independent set. */ class IndependentSetRouter : public RouterBase { std::reference_wrapper architecture_; From 7e5feafa3f11a291cdd1b052fefaad8f18f59786 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:32:55 +0100 Subject: [PATCH 044/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20strict=20router?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/router/IndependentSetRouter.hpp | 4 ++++ .../layout_synthesizer/router/IndependentSetRouter.cpp | 9 +++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index c8e319637..b738b2d38 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -49,6 +49,10 @@ class IndependentSetRouter : public RouterBase { */ RELAXED }; + NLOHMANN_JSON_SERIALIZE_ENUM(Method, { + {Method::STRICT, "strict"}, + {Method::RELAXED, "relaxed"}, + }) Method method = Method::RELAXED; /** * @brief Threshold factor for group merging decisions during routing. diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index ab122c0b6..c78e32f31 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -190,19 +190,16 @@ auto IndependentSetRouter::route(const std::vector& placement) const const auto conflictGraph = createConflictGraph(atomsToMove, startPlacement, targetPlacement); if (config_.method == Config::Method::STRICT) { - struct GroupInfo { - std::vector independentSet; - }; - std::list groups; + auto& currentRouting = routing.emplace_back(); while (!atomsToMove.empty()) { - auto& group = groups.emplace_back(); + auto& group = currentRouting.emplace_back(); std::vector remainingAtoms; std::unordered_set conflictingAtoms; for (const auto& atom : atomsToMove) { if (!conflictingAtoms.contains(atom)) { // if the atom does not conflict with any atom that is already in // the independent set, add it and mark its neighbors as conflicting - group.independentSet.emplace_back(atom); + group.emplace_back(atom); if (const auto conflictingNeighbors = conflictGraph.find(atom); conflictingNeighbors != conflictGraph.end()) { for (const auto neighbor : conflictingNeighbors->second) { From ba1951221808ba10b160fca2be1edaac4c1980ca Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:44:53 +0100 Subject: [PATCH 045/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20json=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 13 ++++++++----- test/na/zoned/test_compiler.cpp | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index b738b2d38..16d0e78fd 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -49,10 +50,6 @@ class IndependentSetRouter : public RouterBase { */ RELAXED }; - NLOHMANN_JSON_SERIALIZE_ENUM(Method, { - {Method::STRICT, "strict"}, - {Method::RELAXED, "relaxed"}, - }) Method method = Method::RELAXED; /** * @brief Threshold factor for group merging decisions during routing. @@ -64,7 +61,7 @@ class IndependentSetRouter : public RouterBase { * @note This value is only relevant if the routing method RELAXED is used. */ double preferSplit = 1.0; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, method, preferSplit); + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, method, preferSplit) }; private: @@ -216,4 +213,10 @@ class IndependentSetRouter : public RouterBase { const std::tuple& w) -> MovementCompatibility; }; +NLOHMANN_JSON_SERIALIZE_ENUM( + IndependentSetRouter::Config::Method, + { + {IndependentSetRouter::Config::Method::STRICT, "strict"}, + {IndependentSetRouter::Config::Method::RELAXED, "relaxed"}, + }) } // namespace na::zoned diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 5b6e2ffb6..8a8b83e62 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -48,7 +48,7 @@ constexpr std::string_view strictRoutingAgnosticConfiguration = R"({ "dynamicPlacement" : true }, "routerConfig" : { - "method" : "STRICT" + "method" : "strict" } }, "codeGeneratorConfig" : { @@ -72,7 +72,7 @@ constexpr std::string_view strictRoutingAwareConfiguration = R"({ "reuseLevel": 5.0 }, "routerConfig" : { - "method" : "STRICT" + "method" : "strict" } } })"; @@ -93,7 +93,7 @@ constexpr std::string_view relaxedRoutingAwareConfiguration = R"({ "reuseLevel": 5.0 }, "routerConfig" : { - "method" : "RELAXED", + "method" : "relaxed", "preferSplit" : 0.0 } } From f72210574934431ba6552719d86bf6ff35846d8b Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:51:28 +0100 Subject: [PATCH 046/119] =?UTF-8?q?=F0=9F=8E=A8=20Expose=20routing=20metho?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 2588957eb..c2b6df758 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include // NOLINTNEXTLINE(misc-include-cleaner) #include @@ -51,6 +52,18 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { return self.exportNAVizMachine(); }); + //===--------------------------------------------------------------------===// + // Routing Method Enum + //===--------------------------------------------------------------------===// + py::native_enum( + m, "Routing Method", "enum.Enum", + "Enumeration of routing methods for the independent set router.") + .value("strict", na::zoned::IndependentSetRouter::Config::Method::STRICT) + .value("relaxed", + na::zoned::IndependentSetRouter::Config::Method::RELAXED) + .export_values() + .finalize(); + //===--------------------------------------------------------------------===// // Routing-agnostic Compiler //===--------------------------------------------------------------------===// @@ -61,6 +74,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { routingAgnosticCompiler.def( py::init([](const na::zoned::Architecture& arch, const std::string& logLevel, const double maxFillingFactor, + const na::zoned::IndependentSetRouter::Config::Method + routingMethod, const bool useWindow, const size_t windowSize, const bool dynamicPlacement, const bool warnUnsupportedGates) @@ -68,6 +83,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { na::zoned::RoutingAgnosticCompiler::Config config; config.logLevel = spdlog::level::from_str(logLevel); config.schedulerConfig.maxFillingFactor = maxFillingFactor; + config.layoutSynthesizerConfig.routerConfig.method = routingMethod; config.layoutSynthesizerConfig.placerConfig = { .useWindow = useWindow, .windowSize = windowSize, @@ -79,6 +95,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { 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, + "routing_method"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.method, "use_window"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.useWindow, "window_size"_a = @@ -121,6 +139,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { routingAwareCompiler.def( py::init([](const na::zoned::Architecture& arch, const std::string& logLevel, const double maxFillingFactor, + const na::zoned::IndependentSetRouter::Config::Method + routingMethod, const bool useWindow, const size_t windowMinWidth, const double windowRatio, const double windowShare, const float deepeningFactor, const float deepeningValue, @@ -130,6 +150,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { na::zoned::RoutingAwareCompiler::Config config; config.logLevel = spdlog::level::from_str(logLevel); config.schedulerConfig.maxFillingFactor = maxFillingFactor; + config.layoutSynthesizerConfig.routerConfig.method = routingMethod; config.layoutSynthesizerConfig.placerConfig = { .useWindow = useWindow, .windowMinWidth = windowMinWidth, @@ -147,6 +168,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { 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, + "routing_method"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.method, "use_window"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.useWindow, "window_min_width"_a = From b1379bdd65c76706481be8bace8ccd935dc68f05 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:57:41 +0100 Subject: [PATCH 047/119] =?UTF-8?q?=F0=9F=8E=A8=20Adopt=20python=20intefac?= =?UTF-8?q?e=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 3 +-- .../router/IndependentSetRouter.hpp | 3 +-- python/mqt/qmap/na/zoned.pyi | 25 ++++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index c2b6df758..de7f0dda8 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -56,8 +56,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { // Routing Method Enum //===--------------------------------------------------------------------===// py::native_enum( - m, "Routing Method", "enum.Enum", - "Enumeration of routing methods for the independent set router.") + m, "RoutingMethod", "enum.Enum") .value("strict", na::zoned::IndependentSetRouter::Config::Method::STRICT) .value("relaxed", na::zoned::IndependentSetRouter::Config::Method::RELAXED) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 16d0e78fd..cef352877 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -34,10 +34,9 @@ class IndependentSetRouter : public RouterBase { public: /** * The configuration of the RelaxedIndependentSetRouter - * @note RelaxedIndependentSetRouter does not have any configuration - * parameters. */ struct Config { + /// The routing method. enum class Method : uint8_t { /** * Use strict routing, i.e., the relative order of atoms must be diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 2cc32c74c..414a72e94 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -8,6 +8,8 @@ """Python bindings module for MQT QMAP's Zoned Neutral Atom Compiler.""" +from enum import Enum + from mqt.core.ir import QuantumComputation class ZonedNeutralAtomArchitecture: @@ -52,6 +54,20 @@ class ZonedNeutralAtomArchitecture: the architecture as a .namachine string """ +class RoutingMethod(Enum): + """Enumeration of the available routing methods for the independent set router.""" + + strict = ... + """ + Strict routing, i.e., the relative order of atoms must be + maintained throughout a movement. + """ + relaxed = ... + """ + Relaxed routing, i.e., the relative order of atoms may change + throughout a movement by applying offsets during pick-up and drop-off. + """ + class RoutingAgnosticCompiler: """MQT QMAP's routing-agnostic Zoned Neutral Atom Compiler.""" @@ -60,6 +76,7 @@ class RoutingAgnosticCompiler: arch: ZonedNeutralAtomArchitecture, log_level: str = ..., max_filling_factor: float = ..., + routing_method: RoutingMethod = ..., use_window: bool = ..., window_size: int = ..., dynamic_placement: bool = ..., @@ -73,6 +90,7 @@ class RoutingAgnosticCompiler: log_level: is the log level for the compiler, possible values are "debug", "info", "warning", "error", "critical" max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel + routing_method: is the routing method that should be used for the independent set router use_window: whether to use a window for the placer window_size: the size of the window for the placer dynamic_placement: whether to use dynamic placement for the placer @@ -117,6 +135,7 @@ class RoutingAwareCompiler: arch: ZonedNeutralAtomArchitecture, log_level: str = ..., max_filling_factor: float = ..., + routing_method: RoutingMethod = ..., use_window: bool = ..., window_min_width: int = ..., window_ratio: float = ..., @@ -135,7 +154,11 @@ class RoutingAwareCompiler: arch: is the zoned neutral atom architecture log_level: is the log level for the compiler, possible values are "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel + max_filling_factor: is the maximum filling factor for the entanglement zone, + i.e., it sets the limit for the maximum number of entangling gates that + are scheduled in parallel + routing_method: is the routing method that should be used for the + independent set router use_window: is a flag whether to use a window for the placer window_min_width: is the minimum width of the window for the placer window_ratio: is the ratio between the height and the width of the window From 3edebd758432ba437e85f58c1be0beb578ade08b Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:06:41 +0100 Subject: [PATCH 048/119] =?UTF-8?q?=F0=9F=92=9A=20Add=20missing=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index de7f0dda8..cbee0a64f 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -15,6 +15,7 @@ #include "na/zoned/layout_synthesizer/PlaceAndRouteSynthesizer.hpp" #include "na/zoned/layout_synthesizer/placer/AStarPlacer.hpp" #include "na/zoned/layout_synthesizer/placer/VertexMatchingPlacer.hpp" +#include "na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp" #include // The header is used, but clang-tidy confuses it with the From acdfa872be185e3903e879c57a3f438c85dda9df Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:24:43 +0100 Subject: [PATCH 049/119] =?UTF-8?q?=F0=9F=8E=A8=20Add=20serialization=20fo?= =?UTF-8?q?r=20placement=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placer/HeuristicPlacer.hpp | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 10276a435..2275a9b8f 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -107,14 +107,14 @@ class HeuristicPlacer : public PlacerBase { */ double windowShare = 0.8; /// Enum of available heuristic methods used for the search. - enum class HeuristicMethod : uint8_t { + enum class Method : uint8_t { /// A-star algorithm - AStar, + ASTAR, /// Iterative diving search IDS }; /// The heuristic method used for the search (default: IDS). - HeuristicMethod heuristicMethod = HeuristicMethod::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 @@ -125,16 +125,14 @@ class HeuristicPlacer : 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 = - heuristicMethod == HeuristicMethod::IDS ? 0.01F : 0.8F; + float deepeningFactor = method == Method::IDS ? 0.01F : 0.8F; /** * @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 = - heuristicMethod == HeuristicMethod::IDS ? 0.0F : 0.2F; + float deepeningValue = method == Method::IDS ? 0.0F : 0.2F; /** * @brief The cost function can consider the distance of atoms to their * interaction partner in the next layer. @@ -143,8 +141,7 @@ class HeuristicPlacer : 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 = - heuristicMethod == HeuristicMethod::IDS ? 0.4F : 0.2F; + float lookaheadFactor = method == Method::IDS ? 0.4F : 0.2F; /** * @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 @@ -966,4 +963,10 @@ class HeuristicPlacer : 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 From 703f9c97c654e469fd366ced2ca09b1ded640645 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:27:21 +0100 Subject: [PATCH 050/119] =?UTF-8?q?=F0=9F=8E=A8=20Expose=20placement=20met?= =?UTF-8?q?hod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 72 ++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 294b18d5a..331c4e79d 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -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 //===--------------------------------------------------------------------===// @@ -137,34 +147,38 @@ 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 na::zoned::IndependentSetRouter::Config::Method - routingMethod, - 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 bool warnUnsupportedGates) - -> na::zoned::RoutingAwareCompiler { - na::zoned::RoutingAwareCompiler::Config config; - config.logLevel = spdlog::level::from_str(logLevel); - config.schedulerConfig.maxFillingFactor = maxFillingFactor; - config.layoutSynthesizerConfig.routerConfig.method = routingMethod; - config.layoutSynthesizerConfig.placerConfig = { - .useWindow = useWindow, - .windowMinWidth = windowMinWidth, - .windowRatio = windowRatio, - .windowShare = windowShare, - .deepeningFactor = deepeningFactor, - .deepeningValue = deepeningValue, - .lookaheadFactor = lookaheadFactor, - .reuseLevel = reuseLevel, - .maxNodes = maxNodes}; - config.codeGeneratorConfig = {.warnUnsupportedGates = - warnUnsupportedGates}; - return {arch, config}; - }), + py::init( + [](const na::zoned::Architecture& arch, const std::string& logLevel, + const double maxFillingFactor, + const na::zoned::IndependentSetRouter::Config::Method + routingMethod, + 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 bool warnUnsupportedGates) + -> na::zoned::RoutingAwareCompiler { + na::zoned::RoutingAwareCompiler::Config config; + config.logLevel = spdlog::level::from_str(logLevel); + config.schedulerConfig.maxFillingFactor = maxFillingFactor; + config.layoutSynthesizerConfig.routerConfig.method = + routingMethod; + config.layoutSynthesizerConfig.placerConfig = { + .useWindow = useWindow, + .windowMinWidth = windowMinWidth, + .windowRatio = windowRatio, + .windowShare = windowShare, + .method = placementMethod, + .deepeningFactor = deepeningFactor, + .deepeningValue = deepeningValue, + .lookaheadFactor = lookaheadFactor, + .reuseLevel = reuseLevel, + .maxNodes = maxNodes}; + 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, @@ -178,6 +192,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 = From b70c80795d64182a2eb6bd6eeafef9528b974032 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:38:11 +0100 Subject: [PATCH 051/119] =?UTF-8?q?=F0=9F=8E=A8=20Adopt=20python=20interfa?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 414a72e94..247302ec4 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.""" @@ -140,6 +152,7 @@ class RoutingAwareCompiler: window_min_width: int = ..., window_ratio: float = ..., window_share: float = ..., + placement_method: PlacementMethod = ..., deepening_factor: float = ..., deepening_value: float = ..., lookahead_factor: float = ..., @@ -164,6 +177,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 From dfc8370c7ba6f0169162c0d1078514f5d3828fbb Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:53:17 +0100 Subject: [PATCH 052/119] =?UTF-8?q?=F0=9F=8E=A8=20Adopting=20the=20tests?= =?UTF-8?q?=20and=20fixing=20minor=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placer/HeuristicPlacer.cpp | 12 +- test/na/zoned/test_compiler.cpp | 160 ++++++++++++++++++ ...r_placer.cpp => test_heuristic_placer.cpp} | 0 3 files changed, 166 insertions(+), 6 deletions(-) rename test/na/zoned/{test_a_star_placer.cpp => test_heuristic_placer.cpp} (100%) diff --git a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp index d921a78ae..504a4ded1 100644 --- a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp +++ b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp @@ -760,8 +760,8 @@ auto HeuristicPlacer::placeGatesInEntanglementZone( const auto deepeningFactor = config_.deepeningFactor; const auto deepeningValue = config_.deepeningValue; std::shared_ptr node; - switch (config_.heuristicMethod) { - case Config::HeuristicMethod::IDS: + switch (config_.method) { + case Config::Method::IDS: node = iterativeDivingSearch( std::make_shared(), [&gateJobs](const auto& node) { return getNeighbors(gateJobs, node); }, @@ -774,7 +774,7 @@ auto HeuristicPlacer::placeGatesInEntanglementZone( }, config_.trials, config_.queueCapacity); break; - case Config::HeuristicMethod::AStar: + case Config::Method::ASTAR: node = aStarTreeSearch( std::make_shared(), [&gateJobs](const auto& node) { return getNeighbors(gateJobs, node); }, @@ -1173,8 +1173,8 @@ auto HeuristicPlacer::placeAtomsInStorageZone( const auto deepeningValue = config_.deepeningValue; std::shared_ptr node; - switch (config_.heuristicMethod) { - case Config::HeuristicMethod::IDS: + switch (config_.method) { + case Config::Method::IDS: node = iterativeDivingSearch( std::make_shared(), [&atomJobs](const auto& node) { return getNeighbors(atomJobs, node); }, @@ -1187,7 +1187,7 @@ auto HeuristicPlacer::placeAtomsInStorageZone( }, config_.trials, config_.queueCapacity); break; - case Config::HeuristicMethod::AStar: + case Config::Method::ASTAR: node = aStarTreeSearch( std::make_shared(), [&atomJobs](const auto& node) { return getNeighbors(atomJobs, node); }, diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 8a8b83e62..b4d839f2e 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,137 @@ 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 = {.method = 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 100% rename from test/na/zoned/test_a_star_placer.cpp rename to test/na/zoned/test_heuristic_placer.cpp From de7ef5e37c71fccf3920244a4cf0b8c5a17362a7 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:54:01 +0100 Subject: [PATCH 053/119] =?UTF-8?q?=F0=9F=93=9D=20Fix=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/router/IndependentSetRouter.hpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index cef352877..8a29a7e14 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -32,9 +32,7 @@ class IndependentSetRouter : public RouterBase { std::reference_wrapper architecture_; public: - /** - * The configuration of the RelaxedIndependentSetRouter - */ + /// The configuration of the IndependentSetRouter struct Config { /// The routing method. enum class Method : uint8_t { @@ -64,11 +62,11 @@ class IndependentSetRouter : public RouterBase { }; private: - /// The configuration of the relaxed independent set router + /// The configuration of the independent set router Config config_; public: - /// Create a RelaxedIndependentSetRouter + /// Create an IndependentSetRouter with the given configuration IndependentSetRouter(const Architecture& architecture, const Config& config) : architecture_(architecture), config_(config) {} /** From b556d1b9af4390e7246e2a14efb2fee7def8d921 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:01:45 +0100 Subject: [PATCH 054/119] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.cpp | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index c78e32f31..4774eb3b3 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -224,6 +224,17 @@ auto IndependentSetRouter::route(const std::vector& placement) const relaxedConflictingAtoms; }; std::list groups; + // Helper to merge conflict costs + auto mergeConflictCost = [](std::optional& existing, + const std::optional& incoming) { + if (existing.has_value()) { + if (incoming.has_value()) { + existing = std::max(*existing, *incoming); + } else { + existing = std::nullopt; + } + } + }; while (!atomsToMove.empty()) { auto& group = groups.emplace_back(); std::vector remainingAtoms; @@ -247,13 +258,8 @@ auto IndependentSetRouter::route(const std::vector& placement) const auto [conflictIt, success] = group.relaxedConflictingAtoms.try_emplace(neighbor.first, neighbor.second); - if (!success && conflictIt->second.has_value()) { - if (neighbor.second.has_value()) { - conflictIt->second = - std::max(*conflictIt->second, *neighbor.second); - } else { - conflictIt->second = std::nullopt; - } + if (!success) { + mergeConflictCost(conflictIt->second, neighbor.second); } } } @@ -344,34 +350,30 @@ auto IndependentSetRouter::route(const std::vector& placement) const // costs, i.e., the distance and the cubed costs directly. if (foundNewGroupForAllAtoms && groupIt->maxDistance > config_.preferSplit * totalCostCubed) { - std::ranges::for_each(atomToNewGroup, [&relaxedConflictGraph, - &atomToDist]( - const auto& pair) { - const auto& [atom, group] = pair; - // add atom to a new group - group->independentSet.emplace_back(atom); - const auto dist = atomToDist.at(atom); - if (group->maxDistance < dist) { - group->maxDistance = dist; - } - if (const auto relaxedConflictingNeighbors = - relaxedConflictGraph.find(atom); - relaxedConflictingNeighbors != relaxedConflictGraph.end()) { - for (const auto neighbor : relaxedConflictingNeighbors->second) { - auto [conflictIt, success] = - group->relaxedConflictingAtoms.try_emplace(neighbor.first, - neighbor.second); - if (!success && conflictIt->second.has_value()) { - if (neighbor.second.has_value()) { - conflictIt->second = - std::max(*conflictIt->second, *neighbor.second); - } else { - conflictIt->second = std::nullopt; + std::ranges::for_each( + atomToNewGroup, [&relaxedConflictGraph, &atomToDist, + &mergeConflictCost](const auto& pair) { + const auto& [atom, group] = pair; + // add atom to a new group + group->independentSet.emplace_back(atom); + const auto dist = atomToDist.at(atom); + if (group->maxDistance < dist) { + group->maxDistance = dist; + } + if (const auto relaxedConflictingNeighbors = + relaxedConflictGraph.find(atom); + relaxedConflictingNeighbors != relaxedConflictGraph.end()) { + for (const auto neighbor : + relaxedConflictingNeighbors->second) { + auto [conflictIt, success] = + group->relaxedConflictingAtoms.try_emplace( + neighbor.first, neighbor.second); + if (!success) { + mergeConflictCost(conflictIt->second, neighbor.second); + } } } - } - } - }); + }); // erase the current group from the linked list of groups; note that // a reverse pointer always points to the element in front of the // current iterator position. From 2b531071cab55e9b3bd12889d7a310287b6f5981 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:03:11 +0100 Subject: [PATCH 055/119] =?UTF-8?q?=F0=9F=93=9D=20Update=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fab293a18..fa9a32c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#340)._ ### Added +- ✨ Add new relaxed routing method to the zoned neutral atom compiler ([#859]) ([**@ystade**]) - 👷 Enable testing on Python 3.14 ([#796]) ([**@denialhaag**]) ### Changed @@ -158,6 +159,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ +[#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 [#804]: https://github.com/munich-quantum-toolkit/qmap/pull/804 From 17fbb32be5235fb9e2576c3f457ef8437e2d2bac Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:59:49 +0100 Subject: [PATCH 056/119] =?UTF-8?q?=F0=9F=93=9D=20Make=20docstring=20more?= =?UTF-8?q?=20comprehensible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/router/IndependentSetRouter.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index 4774eb3b3..641a68909 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -376,9 +376,11 @@ auto IndependentSetRouter::route(const std::vector& placement) const }); // erase the current group from the linked list of groups; note that // a reverse pointer always points to the element in front of the - // current iterator position. - // After erasing, we create a new reverse iterator pointing to the - // same logical position in the remaining list. + // current iterator position. Hence, to erase the current group, we + // increment reverse_iterator to move past the current element, then + // .base() gives forward_iterator to the element to erase. After + // erasing, we create a new reverse iterator pointing to the position + // right before the erased element in the remaining list. const auto& a = (++groupIt).base(); const auto& b = groups.erase(a); groupIt = std::make_reverse_iterator(b); From 41eb559e65664457a4a3dec378c6c55f394a6591 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:00:52 +0100 Subject: [PATCH 057/119] =?UTF-8?q?=F0=9F=93=9D=20Fix=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9a32c8e..edca0595a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ This project adheres to [Semantic Versioning], with the exception that minor rel _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ +### Added + +- ✨ Add relaxed routing method to the zoned neutral atom compiler ([#859]) ([**@ystade**]) + ### Changed - ✨ Enable code generation for relaxed routing constraints ([#848]) ([**@ystade**]) @@ -22,7 +26,6 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#340)._ ### Added -- ✨ Add new relaxed routing method to the zoned neutral atom compiler ([#859]) ([**@ystade**]) - 👷 Enable testing on Python 3.14 ([#796]) ([**@denialhaag**]) ### Changed From a4f4695faaeddffa1a0a96f53b2fc8a25f3b38ad Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:37:26 +0100 Subject: [PATCH 058/119] =?UTF-8?q?=E2=9C=85=20Fix=20a=20star=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placer/HeuristicPlacer.hpp | 2 +- test/na/zoned/test_heuristic_placer.cpp | 119 +++++++++--------- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 2275a9b8f..c350fd631 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -46,7 +46,7 @@ using RowColumnSet = * the placement of the atoms in each layer using a heuristic search algorithm. */ class HeuristicPlacer : public PlacerBase { - friend class HeuristicPlacerPlacerTest_AStarSearch_Test; + friend class HeuristicPlacerTest_AStarSearch_Test; using DiscreteSite = std::array; using CompatibilityGroup = std::array, 2>; diff --git a/test/na/zoned/test_heuristic_placer.cpp b/test/na/zoned/test_heuristic_placer.cpp index 5a063a3ff..f168e335a 100644 --- a/test/na/zoned/test_heuristic_placer.cpp +++ b/test/na/zoned/test_heuristic_placer.cpp @@ -219,7 +219,7 @@ 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)); HeuristicPlacer::Config config = R"({ "useWindow": true, @@ -240,7 +240,7 @@ TEST(AStarPlacerTest, NoSolution) { std::vector>{}), std::runtime_error); } -TEST(AStarPlacerTest, LimitSpace) { +TEST(HeuristicPlacerTest, LimitSpace) { Architecture architecture(Architecture::fromJSONString(architectureJson)); HeuristicPlacer placer(architecture, R"({ "useWindow": true, @@ -261,7 +261,7 @@ TEST(AStarPlacerTest, LimitSpace) { std::vector>{}), std::runtime_error); } -TEST(AStarPlacerTest, WindowExpansion) { +TEST(HeuristicPlacerTest, WindowExpansion) { Architecture architecture(Architecture::fromJSONString(architectureJson)); HeuristicPlacer placer(architecture, R"({ "useWindow": true, @@ -280,7 +280,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": [{ @@ -315,7 +315,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,60 +337,59 @@ TEST(AStarPlacerTest, AStarSearch) { // ┌─────┐ ┌─────┐ ┌Goal=┐ ┌─────┐ // │ 12 ├─────→ │ 13 ├─────→ │ 14 ├─────→ │ 15 │ // └─────┘ └─────┘ └=====┘ └─────┘ - const std::vector nodes(16); - std::unordered_map< - const HeuristicPlacer::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 = - (HeuristicPlacer::aStarTreeSearch( - /* start: */ - nodes[0], - /* getNeighbors: */ - [&neighbors](const HeuristicPlacer::AtomNode& node) - -> std::vector< - std::reference_wrapper> { - return neighbors.at(&node); - }, - /* isGoal: */ - [&nodes](const HeuristicPlacer::AtomNode& node) -> bool { - return &node == &nodes[14]; - }, - /* getCost: */ - [](const HeuristicPlacer::AtomNode& /* unused */) -> double { - return 1.0; - }, - /* getHeuristic: */ - [&nodes](const HeuristicPlacer::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])); + 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: */ + [](std::shared_ptr node) + -> std::vector> { + return node->neighbors; + }, + /* isGoal: */ + [&nodes](const Node& node) -> bool { return &node == nodes[14].get(); }, + /* getCost: */ + [](const Node& /* unused */) -> double { return 1.0; }, + /* getHeuristic: */ + [&nodes](const Node& node) -> double { return node.distanceToGoal; }, + 1'000'000)); + EXPECT_EQ(goal, nodes[14]); } } // namespace na::zoned From 5f07b8d053424fc6ce6bbc8ff8ec29bc59a2d37f Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:41:48 +0100 Subject: [PATCH 059/119] =?UTF-8?q?=F0=9F=8E=A8=20Avoid=20overflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.cpp | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index 641a68909..075f55f2c 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -132,24 +132,22 @@ auto IndependentSetRouter::isRelaxedCompatibleMovement( if (((v0 == w0) != (v2 == w2)) || ((v1 == w1) != (v3 == w3))) { return MovementCompatibility::incompatible(); } + // Helper to safely compute absolute difference + auto distDouble = [](const auto a, const auto b) -> double { + return static_cast(a > b ? a - b : b - a); + }; if ((v0 < w0) != (v2 < w2) && (v1 < w1) != (v3 < w3)) { - return MovementCompatibility::relaxedCompatible(sumCubeRootsCubed( - static_cast( - std::abs(static_cast(v0) - static_cast(w0)) + - std::abs(static_cast(v2) - static_cast(w2))), - static_cast( - std::abs(static_cast(v1) - static_cast(w1)) + - std::abs(static_cast(v3) - static_cast(w3))))); + return MovementCompatibility::relaxedCompatible( + sumCubeRootsCubed(distDouble(v0, w0) + distDouble(v2, w2), + distDouble(v1, w1) + distDouble(v3, w3))); } if ((v0 < w0) != (v2 < w2)) { - return MovementCompatibility::relaxedCompatible(static_cast( - std::abs(static_cast(v0) - static_cast(w0)) + - std::abs(static_cast(v2) - static_cast(w2)))); + return MovementCompatibility::relaxedCompatible(distDouble(v0, w0) + + distDouble(v2, w2)); } if ((v1 < w1) != (v3 < w3)) { - return MovementCompatibility::relaxedCompatible(static_cast( - std::abs(static_cast(v1) - static_cast(w1)) + - std::abs(static_cast(v3) - static_cast(w3)))); + return MovementCompatibility::relaxedCompatible(distDouble(v1, w1) + + distDouble(v3, w3)); } return MovementCompatibility::strictlyCompatible(); } From e80dcf07c4ded8702dad4eb9f640f5075967b0d6 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:47:33 +0100 Subject: [PATCH 060/119] =?UTF-8?q?=F0=9F=8E=A8=20Move=20templated=20funct?= =?UTF-8?q?ion=20to=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placer/HeuristicPlacer.hpp | 132 +++++++++++++++- .../placer/HeuristicPlacer.cpp | 144 ------------------ test/na/zoned/test_heuristic_placer.cpp | 4 +- 3 files changed, 132 insertions(+), 148 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index c350fd631..b07672ee6 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -585,7 +586,76 @@ class HeuristicPlacer : public PlacerBase { const std::function& isGoal, const std::function& getCost, const std::function& getHeuristic, size_t trials, - size_t queueCapacity) -> std::shared_ptr; + const size_t queueCapacity) -> std::shared_ptr { + 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; + } + }; + 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, restart search with priority {}", + goal->priority); + 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 @@ -637,7 +707,65 @@ class HeuristicPlacer : public PlacerBase { const std::function& isGoal, const std::function& getCost, const std::function& getHeuristic, - size_t maxNodes) -> std::shared_ptr; + size_t maxNodes) -> std::shared_ptr { + //===--------------------------------------------------------------------===// + // 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.size() < maxNodes && !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 diff --git a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp index 504a4ded1..e6c59fd02 100644 --- a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp +++ b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp @@ -38,150 +38,6 @@ #include namespace na::zoned { -template -auto HeuristicPlacer::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 { - 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; - } - }; - 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, restart search with priority {}", - goal->priority); - 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; -} -template -auto HeuristicPlacer::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 { - //===--------------------------------------------------------------------===// - // 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.size() < maxNodes && !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."); -} auto HeuristicPlacer::isGoal(const size_t nGates, const GateNode& node) -> bool { return node.level == nGates; diff --git a/test/na/zoned/test_heuristic_placer.cpp b/test/na/zoned/test_heuristic_placer.cpp index f168e335a..09453bae4 100644 --- a/test/na/zoned/test_heuristic_placer.cpp +++ b/test/na/zoned/test_heuristic_placer.cpp @@ -375,7 +375,7 @@ TEST(HeuristicPlacerTest, AStarSearch) { nodes[12]->neighbors = {nodes[13]}; nodes[13]->neighbors = {nodes[14]}; nodes[14]->neighbors = {nodes[15]}; - const auto goal = (HeuristicPlacer::aStarTreeSearch( + const auto goal = HeuristicPlacer::aStarTreeSearch( /* start: */ nodes[0], /* getNeighbors: */ @@ -389,7 +389,7 @@ TEST(HeuristicPlacerTest, AStarSearch) { [](const Node& /* unused */) -> double { return 1.0; }, /* getHeuristic: */ [&nodes](const Node& node) -> double { return node.distanceToGoal; }, - 1'000'000)); + 1'000'000); EXPECT_EQ(goal, nodes[14]); } } // namespace na::zoned From b37d5705b207683c4b6130ea595e9773a8dd7577 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:55:28 +0100 Subject: [PATCH 061/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20placement=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp | 2 +- test/na/zoned/test_heuristic_placer.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index b07672ee6..f506740f2 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/test/na/zoned/test_heuristic_placer.cpp b/test/na/zoned/test_heuristic_placer.cpp index 09453bae4..1da54d773 100644 --- a/test/na/zoned/test_heuristic_placer.cpp +++ b/test/na/zoned/test_heuristic_placer.cpp @@ -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, @@ -226,6 +227,7 @@ TEST(HeuristicPlacerTest, NoSolution) { "windowMinWidth": 0, "windowRatio": 1.0, "windowShare": 0.0, + "method": "astar", "deepeningFactor": 0.6, "deepeningValue": 0.2, "lookaheadFactor": 0.2, @@ -247,6 +249,7 @@ TEST(HeuristicPlacerTest, LimitSpace) { "windowMinWidth": 4, "windowRatio": 1.5, "windowShare": 0.6, + "method": "astar", "deepeningFactor": 0.6, "deepeningValue": 0.2, "lookaheadFactor": 0.2, @@ -268,6 +271,7 @@ TEST(HeuristicPlacerTest, WindowExpansion) { "windowMinWidth": 1, "windowRatio": 1.0, "windowShare": 1.0, + "method": "astar", "deepeningFactor": 0.6, "deepeningValue": 0.2, "lookaheadFactor": 0.2, From 4684fb1606559ab8fa521a8c7830fbb9eaa60380 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:45:27 +0100 Subject: [PATCH 062/119] =?UTF-8?q?=F0=9F=8E=A8=20Split=20routing=20functi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 98 +++- .../router/IndependentSetRouter.cpp | 460 ++++++++++-------- 2 files changed, 344 insertions(+), 214 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 8a29a7e14..1c6f13883 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -84,11 +85,93 @@ class IndependentSetRouter : public RouterBase { -> std::vector override; private: + /** + * Computes a strict routing for the given placement. + * @param placement is the placement for all layers + * @returns a strict routing + */ + [[nodiscard]] auto routeStrict(const std::vector& placement) const + -> std::vector; + /** + * Updates the existing cost if the incoming cost is greater or infinite + * (i.e., std::nullopt). + * @param existing is the existing cost + * @param incoming is the new cost + */ + static auto mergeConflictCost(std::optional& existing, + const std::optional& incoming) -> void; + /// Information about a group during routing + struct GroupInfo { + /// The set of independent atoms in the group + std::vector independentSet; + /// Maximum movement distance of any atom in the group + double maxDistance = 0.0; + /// All atoms that are in conflict with atoms in the independent set + std::unordered_map> + relaxedConflictingAtoms; + }; + /** + * Computes the strict routing before movement groups are merged according to + * the relaxed routing constraints. + * @param atomsToMove is the vector or atoms to move + * @param atomsToDist is a map from atoms to their movement distance + * @param conflictGraph is the conflict graph based on the strict routing + * constraints. + * @param relaxedConflictGraph is the conflict graph based on the relaxed + * routing constraints with weights edges for strict conflicts. + * @returns a list of strict routing groups. + */ + [[nodiscard]] auto makeStrictRoutingForRelaxedRouting( + std::vector atomsToMove, + const std::unordered_map& atomsToDist, + const std::unordered_map>& + conflictGraph, + const std::unordered_map< + qc::Qubit, std::vector>>>& + relaxedConflictGraph) const -> std::list; + /** + * Merges movement groups if all movement of one group can be combined with + * other movement groups based on the relaxed routing constaraints. + * @param atomsToDist is a map from atoms to their movement distance. + * @param relaxedConflictGraph is the conflict graph based on the relaxed + * routing constraints with weights edges for strict conflicts. + * @param groups is a list of movement groups that is modified by this + * function. + */ + auto mergeGroups( + const std::unordered_map& atomsToDist, + const std::unordered_map< + qc::Qubit, std::vector>>>& + relaxedConflictGraph, + std::list& groups) const -> void; + /** + * Computes a relaxed routing for the given placement. + * @param placement is the placement for all layers + * @returns a relaxed routing + */ + [[nodiscard]] auto routeRelaxed(const std::vector& placement) const + -> std::vector; + /** + * @param atomsToDist is a map from atoms to their movement distance. + * @returns the atoms to move, i.e., the keys of the map. + */ + [[nodiscard]] auto + getAtomsToMove(const std::unordered_map& atomsToDist) const + -> std::vector; + /** + * @param startPlacement is the start placement. + * @param targetPlacement is the target placement. + * @returns a map from atoms to their movement distance. + */ + [[nodiscard]] auto + getAtomsToMoveWithDistance(const Placement& startPlacement, + const Placement& targetPlacement) const + -> std::unordered_map; /** * Creates the conflict graph. * @details Atom/qubit indices are the nodes. Two nodes are connected if their * corresponding move with respect to the given @p start- and @p - * targetPlacement stand in conflict with each other. The graph is + * targetPlacement stands in conflict with each other. The graph is * represented as adjacency lists. * @param atomsToMove are all atoms corresponding to nodes in the graph * @param startPlacement is the start placement of all atoms as a mapping from @@ -103,6 +186,19 @@ class IndependentSetRouter : public RouterBase { const Placement& targetPlacement) const -> std::unordered_map>; [[nodiscard]] auto + /** + * Creates the relaxed conflict graph. + * @details Atom/qubit indices are the nodes. Two nodes are connected if their + * corresponding move with respect to the given @p start- and @p + * targetPlacement stands in conflict with each other based on the relaxed + * routing constraints. The graph is represented as adjacency lists. + * @param atomsToMove are all atoms corresponding to nodes in the graph. + * @param startPlacement is the start placement of all atoms as a mapping from + * atoms to their sites. + * @param targetPlacement is the target placement of the atoms. + * @return the conflict graph as an unordered_map, where the keys are the + * nodes and the values are vectors of their neighbors. + */ createRelaxedConflictGraph(const std::vector& atomsToMove, const Placement& startPlacement, const Placement& targetPlacement) const diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index 075f55f2c..8526dcda4 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -153,246 +153,280 @@ auto IndependentSetRouter::isRelaxedCompatibleMovement( } auto IndependentSetRouter::route(const std::vector& placement) const -> std::vector { - std::vector routing; // early return if no placement is given if (placement.empty()) { - return routing; + return {}; + } + if (config_.method == Config::Method::STRICT) { + return routeStrict(placement); } + return routeRelaxed(placement); +} +auto IndependentSetRouter::routeStrict( + const std::vector& placement) const -> std::vector { + std::vector routing; for (size_t i = 0; i + 1 < placement.size(); ++i) { const auto& startPlacement = placement[i]; const auto& targetPlacement = placement[i + 1]; - std::set, std::greater<>> - atomsToMoveOrderedAscByDist; - std::unordered_map atomToDist; - assert(startPlacement.size() == targetPlacement.size()); - for (qc::Qubit atom = 0; atom < startPlacement.size(); ++atom) { - const auto& [startSLM, startRow, startColumn] = startPlacement[atom]; - const auto& [targetSLM, targetRow, targetColumn] = targetPlacement[atom]; - // if atom must be moved - if (&startSLM.get() != &targetSLM.get() || startRow != targetRow || - startColumn != targetColumn) { - const auto distance = - architecture_.get().distance(startSLM, startRow, startColumn, - targetSLM, targetRow, targetColumn); - atomsToMoveOrderedAscByDist.emplace(distance, atom); - atomToDist.emplace(atom, distance); - } - } - std::vector atomsToMove; - atomsToMove.reserve(atomsToMoveOrderedAscByDist.size()); - // put the atoms into the vector such they are ordered decreasingly by their - // movement distance - for (const auto& val : atomsToMoveOrderedAscByDist | std::views::values) { - atomsToMove.emplace_back(val); - } + auto atomsToMove = getAtomsToMove( + getAtomsToMoveWithDistance(startPlacement, targetPlacement)); const auto conflictGraph = createConflictGraph(atomsToMove, startPlacement, targetPlacement); - if (config_.method == Config::Method::STRICT) { - auto& currentRouting = routing.emplace_back(); - while (!atomsToMove.empty()) { - auto& group = currentRouting.emplace_back(); - std::vector remainingAtoms; - std::unordered_set conflictingAtoms; - for (const auto& atom : atomsToMove) { - if (!conflictingAtoms.contains(atom)) { - // if the atom does not conflict with any atom that is already in - // the independent set, add it and mark its neighbors as conflicting - group.emplace_back(atom); - if (const auto conflictingNeighbors = conflictGraph.find(atom); - conflictingNeighbors != conflictGraph.end()) { - for (const auto neighbor : conflictingNeighbors->second) { - conflictingAtoms.emplace(neighbor); - } + auto& currentRouting = routing.emplace_back(); + while (!atomsToMove.empty()) { + auto& group = currentRouting.emplace_back(); + std::vector remainingAtoms; + std::unordered_set conflictingAtoms; + for (const auto& atom : atomsToMove) { + if (!conflictingAtoms.contains(atom)) { + // if the atom does not conflict with any atom that is already in + // the independent set, add it and mark its neighbors as conflicting + group.emplace_back(atom); + if (const auto conflictingNeighbors = conflictGraph.find(atom); + conflictingNeighbors != conflictGraph.end()) { + for (const auto neighbor : conflictingNeighbors->second) { + conflictingAtoms.emplace(neighbor); } - } else { - // if an atom could not be put into the current independent set, add - // it to the remaining atoms - remainingAtoms.emplace_back(atom); } + } else { + // if an atom could not be put into the current independent set, add + // it to the remaining atoms + remainingAtoms.emplace_back(atom); } - atomsToMove = remainingAtoms; } + atomsToMove = remainingAtoms; + } + } + return routing; +} +auto IndependentSetRouter::mergeConflictCost( + std::optional& existing, const std::optional& incoming) + -> void { + if (existing.has_value()) { + if (incoming.has_value()) { + existing = std::max(*existing, *incoming); } else { - const auto relaxedConflictGraph = createRelaxedConflictGraph( - atomsToMove, startPlacement, targetPlacement); - struct GroupInfo { - std::vector independentSet; - double maxDistance = 0.0; - std::unordered_map> - relaxedConflictingAtoms; - }; - std::list groups; - // Helper to merge conflict costs - auto mergeConflictCost = [](std::optional& existing, - const std::optional& incoming) { - if (existing.has_value()) { - if (incoming.has_value()) { - existing = std::max(*existing, *incoming); - } else { - existing = std::nullopt; + existing = std::nullopt; + } + } +} +auto IndependentSetRouter::makeStrictRoutingForRelaxedRouting( + std::vector atomsToMove, + const std::unordered_map& atomsToDist, + const std::unordered_map>& conflictGraph, + const std::unordered_map< + qc::Qubit, std::vector>>>& + relaxedConflictGraph) const -> std::list { + std::list groups; + while (!atomsToMove.empty()) { + auto& group = groups.emplace_back(); + std::vector remainingAtoms; + std::unordered_set conflictingAtoms; + for (const auto& atom : atomsToMove) { + if (!conflictingAtoms.contains(atom)) { + // if the atom does not conflict with any atom that is already in + // the independent set, add it and mark its neighbors as conflicting + group.independentSet.emplace_back(atom); + const auto dist = atomsToDist.at(atom); + if (group.maxDistance < dist) { + group.maxDistance = dist; + } + if (const auto conflictingNeighbors = conflictGraph.find(atom); + conflictingNeighbors != conflictGraph.end()) { + for (const auto neighbor : conflictingNeighbors->second) { + conflictingAtoms.emplace(neighbor); + } + assert(relaxedConflictGraph.contains(atom)); + for (const auto neighbor : relaxedConflictGraph.at(atom)) { + auto [conflictIt, success] = + group.relaxedConflictingAtoms.try_emplace(neighbor.first, + neighbor.second); + if (!success) { + mergeConflictCost(conflictIt->second, neighbor.second); + } } } - }; - while (!atomsToMove.empty()) { - auto& group = groups.emplace_back(); - std::vector remainingAtoms; - std::unordered_set conflictingAtoms; - for (const auto& atom : atomsToMove) { - if (!conflictingAtoms.contains(atom)) { - // if the atom does not conflict with any atom that is already in - // the independent set, add it and mark its neighbors as conflicting - group.independentSet.emplace_back(atom); - const auto dist = atomToDist.at(atom); - if (group.maxDistance < dist) { - group.maxDistance = dist; + } else { + // if an atom could not be put into the current independent set, add + // it to the remaining atoms + remainingAtoms.emplace_back(atom); + } + } + atomsToMove = remainingAtoms; + } + return groups; +} +auto IndependentSetRouter::mergeGroups( + const std::unordered_map& atomsToDist, + const std::unordered_map< + qc::Qubit, std::vector>>>& + relaxedConflictGraph, + std::list& groups) const -> void { + for (auto groupIt = groups.rbegin(); groupIt != groups.rend();) { + const auto& independentSet = groupIt->independentSet; + std::unordered_map atomToNewGroup; + // find the best new group for each qubit in independent set and record + // costs + auto totalCost = 0.0; + auto totalCostCubed = 0.0; + bool foundNewGroupForAllAtoms = true; + for (const auto& atom : independentSet) { + bool foundNewGroup = false; + auto cost = std::numeric_limits::max(); + auto costCubed = std::numeric_limits::max(); + for (auto& group : groups | std::views::reverse) { + // filter current group + if (&group != &*groupIt) { + if (const auto conflictIt = group.relaxedConflictingAtoms.find(atom); + conflictIt == group.relaxedConflictingAtoms.end()) { + const auto dist = atomsToDist.at(atom); + if (group.maxDistance >= dist) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + cost = 0; + costCubed = 0; + break; + } + const auto diff = subCubeRootsCubed(dist, group.maxDistance); + if (costCubed > diff) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = diff; + cost = std::cbrt(diff); } - if (const auto conflictingNeighbors = conflictGraph.find(atom); - conflictingNeighbors != conflictGraph.end()) { - for (const auto neighbor : conflictingNeighbors->second) { - conflictingAtoms.emplace(neighbor); + } else if (conflictIt->second.has_value()) { + // can be added with additional cost because there is only a + // relaxed conflict + const auto dist = atomsToDist.at(atom); + if (group.maxDistance > dist) { + if (costCubed > *conflictIt->second) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = *conflictIt->second; + cost = std::cbrt(*conflictIt->second); } - assert(relaxedConflictGraph.contains(atom)); - for (const auto neighbor : relaxedConflictGraph.at(atom)) { - auto [conflictIt, success] = - group.relaxedConflictingAtoms.try_emplace(neighbor.first, - neighbor.second); - if (!success) { - mergeConflictCost(conflictIt->second, neighbor.second); - } + } else { + const auto c = + sumCubeRootsCubed(subCubeRootsCubed(dist, group.maxDistance), + *conflictIt->second); + if (costCubed > c) { + foundNewGroup = true; + atomToNewGroup.insert_or_assign(atom, &group); + costCubed = c; + cost = std::cbrt(c); } } - } else { - // if an atom could not be put into the current independent set, add - // it to the remaining atoms - remainingAtoms.emplace_back(atom); } } - atomsToMove = remainingAtoms; } - // try to merge rearrangement steps - for (auto groupIt = groups.rbegin(); groupIt != groups.rend();) { - const auto& independentSet = groupIt->independentSet; - std::unordered_map - atomToNewGroup; - // find the best new group for each qubit in independent set and record - // costs - auto totalCost = 0.0; - auto totalCostCubed = 0.0; - bool foundNewGroupForAllAtoms = true; - for (const auto& atom : independentSet) { - bool foundNewGroup = false; - auto cost = std::numeric_limits::max(); - auto costCubed = std::numeric_limits::max(); - for (auto& group : groups | std::views::reverse) { - // filter current group - if (&group != &*groupIt) { - if (const auto conflictIt = - group.relaxedConflictingAtoms.find(atom); - conflictIt == group.relaxedConflictingAtoms.end()) { - const auto dist = atomToDist.at(atom); - if (group.maxDistance >= dist) { - foundNewGroup = true; - atomToNewGroup.insert_or_assign(atom, &group); - cost = 0; - costCubed = 0; - break; - } - const auto diff = subCubeRootsCubed(dist, group.maxDistance); - if (costCubed > diff) { - foundNewGroup = true; - atomToNewGroup.insert_or_assign(atom, &group); - costCubed = diff; - cost = std::cbrt(diff); - } - } else if (conflictIt->second.has_value()) { - // can be added with additional cost because there is only a - // relaxed conflict - const auto dist = atomToDist.at(atom); - if (group.maxDistance > dist) { - if (costCubed > *conflictIt->second) { - foundNewGroup = true; - atomToNewGroup.insert_or_assign(atom, &group); - costCubed = *conflictIt->second; - cost = std::cbrt(*conflictIt->second); - } - } else { - const auto c = sumCubeRootsCubed( - subCubeRootsCubed(dist, group.maxDistance), - *conflictIt->second); - if (costCubed > c) { - foundNewGroup = true; - atomToNewGroup.insert_or_assign(atom, &group); - costCubed = c; - cost = std::cbrt(c); - } - } - } + if (!foundNewGroup) { + foundNewGroupForAllAtoms = false; + break; + } + // Note the following identity to calculate the new cubed offset as + // offsetCostCubed' = (offsetCost + bestCost)^3 + // + // Identity: (x + y)^3 = x^3 + y^3 + 3xy(x+y) + totalCostCubed = costCubed + totalCostCubed + + 3 * cost * totalCost * (cost + totalCost); + totalCost += cost; + } + // if all atoms in the independent set could be assigned to a new group + // and the offset cost, i.e., the time for the extra offsets, is less + // than the cost for the current group. The cost for the current group + // is the cubic root of the distance; hence, we compare the cubes of the + // costs, i.e., the distance and the cubed costs directly. + if (foundNewGroupForAllAtoms && + groupIt->maxDistance > config_.preferSplit * totalCostCubed) { + std::ranges::for_each(atomToNewGroup, [&relaxedConflictGraph, + &atomsToDist](const auto& pair) { + const auto& [atom, group] = pair; + // add atom to a new group + group->independentSet.emplace_back(atom); + const auto dist = atomsToDist.at(atom); + if (group->maxDistance < dist) { + group->maxDistance = dist; + } + if (const auto relaxedConflictingNeighbors = + relaxedConflictGraph.find(atom); + relaxedConflictingNeighbors != relaxedConflictGraph.end()) { + for (const auto neighbor : relaxedConflictingNeighbors->second) { + auto [conflictIt, success] = + group->relaxedConflictingAtoms.try_emplace(neighbor.first, + neighbor.second); + if (!success) { + mergeConflictCost(conflictIt->second, neighbor.second); } } - if (!foundNewGroup) { - foundNewGroupForAllAtoms = false; - break; - } - // Note the following identity to calculate the new cubed offset as - // offsetCostCubed' = (offsetCost + bestCost)^3 - // - // Identity: (x + y)^3 = x^3 + y^3 + 3xy(x+y) - totalCostCubed = costCubed + totalCostCubed + - 3 * cost * totalCost * (cost + totalCost); - totalCost += cost; - } - // if all atoms in the independent set could be assigned to a new group - // and the offset cost, i.e., the time for the extra offsets, is less - // than the cost for the current group. The cost for the current group - // is the cubic root of the distance; hence, we compare the cubes of the - // costs, i.e., the distance and the cubed costs directly. - if (foundNewGroupForAllAtoms && - groupIt->maxDistance > config_.preferSplit * totalCostCubed) { - std::ranges::for_each( - atomToNewGroup, [&relaxedConflictGraph, &atomToDist, - &mergeConflictCost](const auto& pair) { - const auto& [atom, group] = pair; - // add atom to a new group - group->independentSet.emplace_back(atom); - const auto dist = atomToDist.at(atom); - if (group->maxDistance < dist) { - group->maxDistance = dist; - } - if (const auto relaxedConflictingNeighbors = - relaxedConflictGraph.find(atom); - relaxedConflictingNeighbors != relaxedConflictGraph.end()) { - for (const auto neighbor : - relaxedConflictingNeighbors->second) { - auto [conflictIt, success] = - group->relaxedConflictingAtoms.try_emplace( - neighbor.first, neighbor.second); - if (!success) { - mergeConflictCost(conflictIt->second, neighbor.second); - } - } - } - }); - // erase the current group from the linked list of groups; note that - // a reverse pointer always points to the element in front of the - // current iterator position. Hence, to erase the current group, we - // increment reverse_iterator to move past the current element, then - // .base() gives forward_iterator to the element to erase. After - // erasing, we create a new reverse iterator pointing to the position - // right before the erased element in the remaining list. - const auto& a = (++groupIt).base(); - const auto& b = groups.erase(a); - groupIt = std::make_reverse_iterator(b); - } else { - ++groupIt; } - } - auto& currentRouting = routing.emplace_back(); - currentRouting.reserve(groups.size()); - for (auto& group : groups) { - currentRouting.emplace_back(std::move(group.independentSet)); - } + }); + // erase the current group from the linked list of groups; note that + // a reverse pointer always points to the element in front of the + // current iterator position. Hence, to erase the current group, we + // increment reverse_iterator to move past the current element, then + // .base() gives forward_iterator to the element to erase. After + // erasing, we create a new reverse iterator pointing to the position + // right before the erased element in the remaining list. + const auto& a = (++groupIt).base(); + const auto& b = groups.erase(a); + groupIt = std::make_reverse_iterator(b); + } else { + ++groupIt; + } + } +} +auto IndependentSetRouter::routeRelaxed( + const std::vector& placement) const -> std::vector { + std::vector routing; + for (size_t i = 0; i + 1 < placement.size(); ++i) { + const auto& startPlacement = placement[i]; + const auto& targetPlacement = placement[i + 1]; + const auto& atomsToDist = + getAtomsToMoveWithDistance(startPlacement, targetPlacement); + auto atomsToMove = getAtomsToMove(atomsToDist); + const auto conflictGraph = + createConflictGraph(atomsToMove, startPlacement, targetPlacement); + const auto relaxedConflictGraph = createRelaxedConflictGraph( + atomsToMove, startPlacement, targetPlacement); + auto groups = makeStrictRoutingForRelaxedRouting( + atomsToMove, atomsToDist, conflictGraph, relaxedConflictGraph); + mergeGroups(atomsToDist, relaxedConflictGraph, groups); + auto& currentRouting = routing.emplace_back(); + currentRouting.reserve(groups.size()); + for (auto& group : groups) { + currentRouting.emplace_back(std::move(group.independentSet)); } } return routing; } +auto IndependentSetRouter::getAtomsToMove( + const std::unordered_map& atomsToDist) const + -> std::vector { + std::vector atomsToMove; + atomsToMove.reserve(atomsToDist.size()); + std::ranges::copy(atomsToDist | std::views::keys, + std::back_inserter(atomsToMove)); + return atomsToMove; +} +auto IndependentSetRouter::getAtomsToMoveWithDistance( + const Placement& startPlacement, const Placement& targetPlacement) const + -> std::unordered_map { + std::set, std::greater<>> + atomsToMoveOrderedAscByDist; + std::unordered_map atomsToDist; + assert(startPlacement.size() == targetPlacement.size()); + for (qc::Qubit atom = 0; atom < startPlacement.size(); ++atom) { + const auto& [startSLM, startRow, startColumn] = startPlacement[atom]; + const auto& [targetSLM, targetRow, targetColumn] = targetPlacement[atom]; + // if atom must be moved + if (&startSLM.get() != &targetSLM.get() || startRow != targetRow || + startColumn != targetColumn) { + const auto distance = architecture_.get().distance( + startSLM, startRow, startColumn, targetSLM, targetRow, targetColumn); + atomsToMoveOrderedAscByDist.emplace(distance, atom); + atomsToDist.emplace(atom, distance); + } + } + return atomsToDist; +} } // namespace na::zoned From a39d26778be707214c50ffb648b067aeb02f93a5 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:55:35 +0100 Subject: [PATCH 063/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/placer/HeuristicPlacer.hpp | 10 ++++------ test/na/zoned/test_heuristic_placer.cpp | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index f506740f2..d9a1fe6e5 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -170,12 +170,10 @@ class HeuristicPlacer : public PlacerBase { * @note This option is only relevant if the IDS heuristic method is used. */ size_t queueCapacity = 100; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, useWindow, - windowMinWidth, windowRatio, - windowShare, deepeningFactor, - deepeningValue, lookaheadFactor, - reuseLevel, maxNodes, trials, - queueCapacity); + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( + Config, useWindow, windowMinWidth, windowRatio, windowShare, method, + deepeningFactor, deepeningValue, lookaheadFactor, reuseLevel, maxNodes, + trials, queueCapacity); }; private: diff --git a/test/na/zoned/test_heuristic_placer.cpp b/test/na/zoned/test_heuristic_placer.cpp index 1da54d773..92201528d 100644 --- a/test/na/zoned/test_heuristic_placer.cpp +++ b/test/na/zoned/test_heuristic_placer.cpp @@ -256,11 +256,11 @@ TEST(HeuristicPlacerTest, LimitSpace) { "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); } From f3e43992ec6e1088a3c93489d02e7aa6572592ac Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 6 Dec 2025 10:45:11 +0100 Subject: [PATCH 064/119] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Micro=20optimizati?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index 8526dcda4..ced3148de 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -298,7 +298,7 @@ auto IndependentSetRouter::mergeGroups( // can be added with additional cost because there is only a // relaxed conflict const auto dist = atomsToDist.at(atom); - if (group.maxDistance > dist) { + if (group.maxDistance >= dist) { if (costCubed > *conflictIt->second) { foundNewGroup = true; atomToNewGroup.insert_or_assign(atom, &group); From 342b89d90a9ef567945548d1c8512c12314cbfd0 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 6 Dec 2025 10:46:58 +0100 Subject: [PATCH 065/119] =?UTF-8?q?=F0=9F=92=9A=20Add=20missing=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoned/layout_synthesizer/router/IndependentSetRouter.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 1c6f13883..89fc37abe 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -16,11 +16,14 @@ #include "na/zoned/layout_synthesizer/router/RouterBase.hpp" #include +#include #include #include #include +#include #include #include +#include #include namespace na::zoned { From a102c6b4e945ea7833b6e3033120c7d99e4aa5f0 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 6 Dec 2025 10:50:32 +0100 Subject: [PATCH 066/119] =?UTF-8?q?=F0=9F=93=9D=20Make=20docstrings=20more?= =?UTF-8?q?=20precise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 89fc37abe..de2dab69c 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -59,7 +59,8 @@ class IndependentSetRouter : public RouterBase { * groups based on the relaxed constraints. Higher values of this * parameter favor keeping groups separate; lower values favor merging. * In particular, a value of 0.0 merges all possible groups. (Default: 1.0) - * @note This value is only relevant if the routing method RELAXED is used. + * @note This value is only relevant if the routing method RELAXED is used + * and ignored otherwise. */ double preferSplit = 1.0; NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, method, preferSplit) @@ -121,7 +122,7 @@ class IndependentSetRouter : public RouterBase { * @param conflictGraph is the conflict graph based on the strict routing * constraints. * @param relaxedConflictGraph is the conflict graph based on the relaxed - * routing constraints with weights edges for strict conflicts. + * routing constraints with weighted edges for strict conflicts. * @returns a list of strict routing groups. */ [[nodiscard]] auto makeStrictRoutingForRelaxedRouting( @@ -137,7 +138,7 @@ class IndependentSetRouter : public RouterBase { * other movement groups based on the relaxed routing constaraints. * @param atomsToDist is a map from atoms to their movement distance. * @param relaxedConflictGraph is the conflict graph based on the relaxed - * routing constraints with weights edges for strict conflicts. + * routing constraints with weighted edges for strict conflicts. * @param groups is a list of movement groups that is modified by this * function. */ @@ -200,7 +201,10 @@ class IndependentSetRouter : public RouterBase { * atoms to their sites. * @param targetPlacement is the target placement of the atoms. * @return the conflict graph as an unordered_map, where the keys are the - * nodes and the values are vectors of their neighbors. + * nodes and the values are vectors of their neighbors together with an + * optional associated cost for merging the movements in case of a strict + * conflict that is not a conflict with respect to the relaxed routing + * constraints. */ createRelaxedConflictGraph(const std::vector& atomsToMove, const Placement& startPlacement, From b6176588e0f2205c33be1b9570e183c3b1572380 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 6 Dec 2025 10:52:10 +0100 Subject: [PATCH 067/119] =?UTF-8?q?=F0=9F=93=9D=20Mention=20preferSPlit=20?= =?UTF-8?q?parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoned/layout_synthesizer/router/IndependentSetRouter.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index de2dab69c..21ddceb66 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -135,7 +135,8 @@ class IndependentSetRouter : public RouterBase { relaxedConflictGraph) const -> std::list; /** * Merges movement groups if all movement of one group can be combined with - * other movement groups based on the relaxed routing constaraints. + * other movement groups based on the relaxed routing constraints. + * The merging decisions are driven by the configured `preferSplit` threshold. * @param atomsToDist is a map from atoms to their movement distance. * @param relaxedConflictGraph is the conflict graph based on the relaxed * routing constraints with weighted edges for strict conflicts. From 837b3934b02fb65ef75a05876f9d6204d28eeb33 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 6 Dec 2025 11:01:27 +0100 Subject: [PATCH 068/119] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Improve=20performa?= =?UTF-8?q?nce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 29 +++++++------ .../router/IndependentSetRouter.cpp | 42 +++++++++++-------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 21ddceb66..f45d5828d 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -125,14 +125,14 @@ class IndependentSetRouter : public RouterBase { * routing constraints with weighted edges for strict conflicts. * @returns a list of strict routing groups. */ - [[nodiscard]] auto makeStrictRoutingForRelaxedRouting( + [[nodiscard]] static auto makeStrictRoutingForRelaxedRouting( std::vector atomsToMove, const std::unordered_map& atomsToDist, const std::unordered_map>& conflictGraph, const std::unordered_map< qc::Qubit, std::vector>>>& - relaxedConflictGraph) const -> std::list; + relaxedConflictGraph) -> std::list; /** * Merges movement groups if all movement of one group can be combined with * other movement groups based on the relaxed routing constraints. @@ -173,7 +173,7 @@ class IndependentSetRouter : public RouterBase { const Placement& targetPlacement) const -> std::unordered_map; /** - * Creates the conflict graph. + * Creates the conflict graph with respect to the strict routing constraints. * @details Atom/qubit indices are the nodes. Two nodes are connected if their * corresponding move with respect to the given @p start- and @p * targetPlacement stands in conflict with each other. The graph is @@ -186,13 +186,12 @@ class IndependentSetRouter : public RouterBase { * nodes and the values are vectors of their neighbors */ [[nodiscard]] auto - createConflictGraph(const std::vector& atomsToMove, - const Placement& startPlacement, - const Placement& targetPlacement) const + createStrictConflictGraph(const std::vector& atomsToMove, + const Placement& startPlacement, + const Placement& targetPlacement) const -> std::unordered_map>; - [[nodiscard]] auto /** - * Creates the relaxed conflict graph. + * Creates the relaxed and strict conflict graph. * @details Atom/qubit indices are the nodes. Two nodes are connected if their * corresponding move with respect to the given @p start- and @p * targetPlacement stands in conflict with each other based on the relaxed @@ -207,11 +206,15 @@ class IndependentSetRouter : public RouterBase { * conflict that is not a conflict with respect to the relaxed routing * constraints. */ - createRelaxedConflictGraph(const std::vector& atomsToMove, - const Placement& startPlacement, - const Placement& targetPlacement) const - -> std::unordered_map< - qc::Qubit, std::vector>>>; + [[nodiscard]] auto + createRelaxedAndStrictConflictGraph(const std::vector& atomsToMove, + const Placement& startPlacement, + const Placement& targetPlacement) const + -> std::pair< + std::unordered_map>, + std::unordered_map< + qc::Qubit, + std::vector>>>>; /** * Takes two sites, the start and target site and returns a 4D-vector of the diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index ced3148de..c07cf68f7 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -25,7 +25,7 @@ #include namespace na::zoned { -auto IndependentSetRouter::createConflictGraph( +auto IndependentSetRouter::createStrictConflictGraph( const std::vector& atomsToMove, const Placement& startPlacement, const Placement& targetPlacement) const -> std::unordered_map> { @@ -48,14 +48,17 @@ auto IndependentSetRouter::createConflictGraph( } return conflictGraph; } -auto IndependentSetRouter::createRelaxedConflictGraph( +auto IndependentSetRouter::createRelaxedAndStrictConflictGraph( const std::vector& atomsToMove, const Placement& startPlacement, const Placement& targetPlacement) const - -> std::unordered_map< - qc::Qubit, std::vector>>> { + -> std::pair< + std::unordered_map>, + std::unordered_map>>>> { + std::unordered_map> strictConflictGraph; std::unordered_map>>> - conflictGraph; + relaxedConflictGraph; for (auto atomIt = atomsToMove.cbegin(); atomIt != atomsToMove.cend(); ++atomIt) { const auto& atom = *atomIt; @@ -66,17 +69,21 @@ auto IndependentSetRouter::createRelaxedConflictGraph( const auto& neighbor = *neighborIt; const auto& neighborMovementVector = getMovementVector( startPlacement[neighbor], targetPlacement[neighbor]); - if (const auto& comp = isRelaxedCompatibleMovement( - atomMovementVector, neighborMovementVector); - comp.status != MovementCompatibility::Status::StrictlyCompatible) { - conflictGraph.try_emplace(atom).first->second.emplace_back( + const auto& comp = isRelaxedCompatibleMovement(atomMovementVector, + neighborMovementVector); + if (comp.status != MovementCompatibility::Status::StrictlyCompatible) { + strictConflictGraph.try_emplace(atom).first->second.emplace_back( + neighbor); + strictConflictGraph.try_emplace(neighbor).first->second.emplace_back( + atom); + relaxedConflictGraph.try_emplace(atom).first->second.emplace_back( neighbor, comp.mergeCost); - conflictGraph.try_emplace(neighbor).first->second.emplace_back( + relaxedConflictGraph.try_emplace(neighbor).first->second.emplace_back( atom, comp.mergeCost); } } } - return conflictGraph; + return {strictConflictGraph, relaxedConflictGraph}; } auto IndependentSetRouter::getMovementVector( const std::tuple& start, @@ -171,7 +178,7 @@ auto IndependentSetRouter::routeStrict( auto atomsToMove = getAtomsToMove( getAtomsToMoveWithDistance(startPlacement, targetPlacement)); const auto conflictGraph = - createConflictGraph(atomsToMove, startPlacement, targetPlacement); + createStrictConflictGraph(atomsToMove, startPlacement, targetPlacement); auto& currentRouting = routing.emplace_back(); while (!atomsToMove.empty()) { auto& group = currentRouting.emplace_back(); @@ -216,7 +223,7 @@ auto IndependentSetRouter::makeStrictRoutingForRelaxedRouting( const std::unordered_map>& conflictGraph, const std::unordered_map< qc::Qubit, std::vector>>>& - relaxedConflictGraph) const -> std::list { + relaxedConflictGraph) -> std::list { std::list groups; while (!atomsToMove.empty()) { auto& group = groups.emplace_back(); @@ -384,12 +391,11 @@ auto IndependentSetRouter::routeRelaxed( const auto& atomsToDist = getAtomsToMoveWithDistance(startPlacement, targetPlacement); auto atomsToMove = getAtomsToMove(atomsToDist); - const auto conflictGraph = - createConflictGraph(atomsToMove, startPlacement, targetPlacement); - const auto relaxedConflictGraph = createRelaxedConflictGraph( - atomsToMove, startPlacement, targetPlacement); + const auto& [strictConflictGraph, relaxedConflictGraph] = + createRelaxedAndStrictConflictGraph(atomsToMove, startPlacement, + targetPlacement); auto groups = makeStrictRoutingForRelaxedRouting( - atomsToMove, atomsToDist, conflictGraph, relaxedConflictGraph); + atomsToMove, atomsToDist, strictConflictGraph, relaxedConflictGraph); mergeGroups(atomsToDist, relaxedConflictGraph, groups); auto& currentRouting = routing.emplace_back(); currentRouting.reserve(groups.size()); From d35f58d811a08bb278cad1eadd72f848b43b845b Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 6 Dec 2025 11:03:46 +0100 Subject: [PATCH 069/119] =?UTF-8?q?=F0=9F=93=9D=20Provide=20defaults=20and?= =?UTF-8?q?d=20improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index f45d5828d..fed570c84 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -230,12 +230,14 @@ class IndependentSetRouter : public RouterBase { -> std::tuple; /** - * Check whether two movements are compatible, i.e., the topological order - * of the moved atoms remain the same. + * Check the compatibility of two movements under the relaxed routing + * constraints. The movements may change topological order (via offsets) but + * must still satisfy the relaxed row/column conditions. * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) * @param w is the other 4D-vector of the form (x-start, y-start, x-end, * y-end) - * @return true, if the given movement vectors are compatible, otherwise false + * @returns a @ref MovementCompatibility object indicating whether they are + * strictly compatible, relaxed compatible (with merge cost), or incompatible. */ [[nodiscard]] static auto isCompatibleMovement(const std::tuple& v, @@ -271,7 +273,7 @@ class IndependentSetRouter : public RouterBase { }; /// Indicates the type of compatibility - Status status; + Status status = Status::Incompatible; /** * @brief In the case of `RelaxedCompatible`, the cost to merge the two * movements. @@ -286,7 +288,7 @@ class IndependentSetRouter : public RouterBase { * then cubed again. * Hence, the cost must always be a non-negative number. */ - std::optional mergeCost; + std::optional mergeCost = std::nullopt; /// Factory methods for strict compatibility [[nodiscard]] static auto strictlyCompatible() -> MovementCompatibility { From d301fd3afb4589fcb6fd9e6f6805290f16bf0b5c Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 6 Dec 2025 11:08:38 +0100 Subject: [PATCH 070/119] =?UTF-8?q?=F0=9F=93=9D=20Improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index fed570c84..8840ea22b 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -194,17 +194,20 @@ class IndependentSetRouter : public RouterBase { * Creates the relaxed and strict conflict graph. * @details Atom/qubit indices are the nodes. Two nodes are connected if their * corresponding move with respect to the given @p start- and @p - * targetPlacement stands in conflict with each other based on the relaxed + * targetPlacement stands in conflict with each other based on the strict * routing constraints. The graph is represented as adjacency lists. + * @par + * * In contrast to the strict conflict graph, all edges that do not represent + * a conflict with respect to the relaxed routing constraints carry a weight. + * The weight of other edges is `std::nullopt`. This weight corresponds to the + * cost for merging the two adjacent movements. * @param atomsToMove are all atoms corresponding to nodes in the graph. * @param startPlacement is the start placement of all atoms as a mapping from * atoms to their sites. * @param targetPlacement is the target placement of the atoms. - * @return the conflict graph as an unordered_map, where the keys are the - * nodes and the values are vectors of their neighbors together with an - * optional associated cost for merging the movements in case of a strict - * conflict that is not a conflict with respect to the relaxed routing - * constraints. + * @return the relaxed conflict graph as an unordered_map where the keys are + * the nodes and the values are vectors of (neighbor, optional merge cost) + * pairs. */ [[nodiscard]] auto createRelaxedAndStrictConflictGraph(const std::vector& atomsToMove, From 039797bccdd4437b70a330b00b387e03a7c650d7 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:34:18 +0100 Subject: [PATCH 071/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20docstring=20and=20?= =?UTF-8?q?ordering=20of=20atoms=20to=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 27 +++++++------ .../router/IndependentSetRouter.cpp | 39 ++++++++++++++----- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 8840ea22b..638d6a0d9 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -157,21 +157,24 @@ class IndependentSetRouter : public RouterBase { [[nodiscard]] auto routeRelaxed(const std::vector& placement) const -> std::vector; /** - * @param atomsToDist is a map from atoms to their movement distance. - * @returns the atoms to move, i.e., the keys of the map. + * @param startPlacement is the start placement. + * @param targetPlacement is the target placement. + * @returns the atoms to move */ - [[nodiscard]] auto - getAtomsToMove(const std::unordered_map& atomsToDist) const + [[nodiscard]] auto getAtomsToMove(const Placement& startPlacement, + const Placement& targetPlacement) const -> std::vector; /** * @param startPlacement is the start placement. * @param targetPlacement is the target placement. - * @returns a map from atoms to their movement distance. + * @returns a pair consisting of a vector containing the atoms to move and a + * map from atoms to their movement distance. */ [[nodiscard]] auto getAtomsToMoveWithDistance(const Placement& startPlacement, const Placement& targetPlacement) const - -> std::unordered_map; + -> std::pair, + std::unordered_map>; /** * Creates the conflict graph with respect to the strict routing constraints. * @details Atom/qubit indices are the nodes. Two nodes are connected if their @@ -233,14 +236,13 @@ class IndependentSetRouter : public RouterBase { -> std::tuple; /** - * Check the compatibility of two movements under the relaxed routing - * constraints. The movements may change topological order (via offsets) but - * must still satisfy the relaxed row/column conditions. + * Check whether two movements are strictly compatible, i.e., atoms remain on + * the same row (column) and maintain their relative/topological order. * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) * @param w is the other 4D-vector of the form (x-start, y-start, x-end, * y-end) - * @returns a @ref MovementCompatibility object indicating whether they are - * strictly compatible, relaxed compatible (with merge cost), or incompatible. + * @returns `true` if the movements are (strictly) compatible, `false` + * otherwise. */ [[nodiscard]] static auto isCompatibleMovement(const std::tuple& v, @@ -315,7 +317,8 @@ class IndependentSetRouter : public RouterBase { * @param v is a 4D-vector of the form (x-start, y-start, x-end, y-end) * @param w is the other 4D-vector of the form (x-start, y-start, x-end, * y-end) - * @returns a @ref MovementCompatibility object indicating the compatibility. + * @returns a @ref MovementCompatibility object indicating whether they are + * strictly compatible, relaxed compatible (with merge cost), or incompatible. */ [[nodiscard]] static auto isRelaxedCompatibleMovement( const std::tuple& v, diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index c07cf68f7..b1811e24d 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -175,8 +175,7 @@ auto IndependentSetRouter::routeStrict( for (size_t i = 0; i + 1 < placement.size(); ++i) { const auto& startPlacement = placement[i]; const auto& targetPlacement = placement[i + 1]; - auto atomsToMove = getAtomsToMove( - getAtomsToMoveWithDistance(startPlacement, targetPlacement)); + auto atomsToMove = getAtomsToMove(startPlacement, targetPlacement); const auto conflictGraph = createStrictConflictGraph(atomsToMove, startPlacement, targetPlacement); auto& currentRouting = routing.emplace_back(); @@ -388,9 +387,8 @@ auto IndependentSetRouter::routeRelaxed( for (size_t i = 0; i + 1 < placement.size(); ++i) { const auto& startPlacement = placement[i]; const auto& targetPlacement = placement[i + 1]; - const auto& atomsToDist = + auto [atomsToMove, atomsToDist] = getAtomsToMoveWithDistance(startPlacement, targetPlacement); - auto atomsToMove = getAtomsToMove(atomsToDist); const auto& [strictConflictGraph, relaxedConflictGraph] = createRelaxedAndStrictConflictGraph(atomsToMove, startPlacement, targetPlacement); @@ -406,17 +404,34 @@ auto IndependentSetRouter::routeRelaxed( return routing; } auto IndependentSetRouter::getAtomsToMove( - const std::unordered_map& atomsToDist) const + const Placement& startPlacement, const Placement& targetPlacement) const -> std::vector { + std::set, std::greater<>> + atomsToMoveOrderedAscByDist; + assert(startPlacement.size() == targetPlacement.size()); + for (qc::Qubit atom = 0; atom < startPlacement.size(); ++atom) { + const auto& [startSLM, startRow, startColumn] = startPlacement[atom]; + const auto& [targetSLM, targetRow, targetColumn] = targetPlacement[atom]; + // if atom must be moved + if (&startSLM.get() != &targetSLM.get() || startRow != targetRow || + startColumn != targetColumn) { + const auto distance = architecture_.get().distance( + startSLM, startRow, startColumn, targetSLM, targetRow, targetColumn); + atomsToMoveOrderedAscByDist.emplace(distance, atom); + } + } std::vector atomsToMove; - atomsToMove.reserve(atomsToDist.size()); - std::ranges::copy(atomsToDist | std::views::keys, + atomsToMove.reserve(atomsToMoveOrderedAscByDist.size()); + // put the atoms into the vector such they are ordered decreasingly by their + // movement distance + std::ranges::copy(atomsToMoveOrderedAscByDist | std::views::values, std::back_inserter(atomsToMove)); return atomsToMove; } auto IndependentSetRouter::getAtomsToMoveWithDistance( const Placement& startPlacement, const Placement& targetPlacement) const - -> std::unordered_map { + -> std::pair, + std::unordered_map> { std::set, std::greater<>> atomsToMoveOrderedAscByDist; std::unordered_map atomsToDist; @@ -433,6 +448,12 @@ auto IndependentSetRouter::getAtomsToMoveWithDistance( atomsToDist.emplace(atom, distance); } } - return atomsToDist; + std::vector atomsToMove; + atomsToMove.reserve(atomsToMoveOrderedAscByDist.size()); + // put the atoms into the vector such they are ordered decreasingly by their + // movement distance + std::ranges::copy(atomsToMoveOrderedAscByDist | std::views::values, + std::back_inserter(atomsToMove)); + return {atomsToMove, atomsToDist}; } } // namespace na::zoned From a9274c7729bd55b1fa5443376f7fc63bcaad7f45 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:58:52 +0100 Subject: [PATCH 072/119] =?UTF-8?q?=F0=9F=93=9D=20Improve=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index 638d6a0d9..fed2dab2b 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -68,7 +68,7 @@ class IndependentSetRouter : public RouterBase { private: /// The configuration of the independent set router - Config config_; + const Config config_; public: /// Create an IndependentSetRouter with the given configuration @@ -97,8 +97,8 @@ class IndependentSetRouter : public RouterBase { [[nodiscard]] auto routeStrict(const std::vector& placement) const -> std::vector; /** - * Updates the existing cost if the incoming cost is greater or infinite - * (i.e., std::nullopt). + * Updates the existing cost if the incoming cost is greater or indicating + * incompatibility (`std::nullopt`), which dominates any finite cost. * @param existing is the existing cost * @param incoming is the new cost */ @@ -201,9 +201,11 @@ class IndependentSetRouter : public RouterBase { * routing constraints. The graph is represented as adjacency lists. * @par * * In contrast to the strict conflict graph, all edges that do not represent - * a conflict with respect to the relaxed routing constraints carry a weight. - * The weight of other edges is `std::nullopt`. This weight corresponds to the - * cost for merging the two adjacent movements. + * a conflict with respect to the relaxed routing constraints carry a weight + * representing the cost for merging the two adjacent movements. These edges + * correspond to the value `RelaxedCompatible` returned by @ref + * isRelaxedCompatibleMovement. The weight of other edges, i.e., + * `Incompatible`, is `std::nullopt`. * @param atomsToMove are all atoms corresponding to nodes in the graph. * @param startPlacement is the start placement of all atoms as a mapping from * atoms to their sites. From 4b826796d75e4369e48592f0d030096d2e39030d Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:38:06 +0100 Subject: [PATCH 073/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20mac=20os=2014=20bu?= =?UTF-8?q?ild?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index d9a1fe6e5..9115a7729 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -352,6 +352,10 @@ class HeuristicPlacer : public PlacerBase { 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_; From a72dafe17dd59425e788c25d03cdf72a3770a540 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:39:16 +0100 Subject: [PATCH 074/119] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20superfluous=20c?= =?UTF-8?q?apture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/na/zoned/test_heuristic_placer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/na/zoned/test_heuristic_placer.cpp b/test/na/zoned/test_heuristic_placer.cpp index 92201528d..fe38a0d90 100644 --- a/test/na/zoned/test_heuristic_placer.cpp +++ b/test/na/zoned/test_heuristic_placer.cpp @@ -392,7 +392,7 @@ TEST(HeuristicPlacerTest, AStarSearch) { /* getCost: */ [](const Node& /* unused */) -> double { return 1.0; }, /* getHeuristic: */ - [&nodes](const Node& node) -> double { return node.distanceToGoal; }, + [](const Node& node) -> double { return node.distanceToGoal; }, 1'000'000); EXPECT_EQ(goal, nodes[14]); } From fb88af187a38ce4d0c8dd507a01623b2de351670 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:41:57 +0100 Subject: [PATCH 075/119] =?UTF-8?q?=F0=9F=8E=A8=20Micro=20opt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index b1811e24d..55ac7cee0 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -356,7 +356,7 @@ auto IndependentSetRouter::mergeGroups( if (const auto relaxedConflictingNeighbors = relaxedConflictGraph.find(atom); relaxedConflictingNeighbors != relaxedConflictGraph.end()) { - for (const auto neighbor : relaxedConflictingNeighbors->second) { + for (const auto& neighbor : relaxedConflictingNeighbors->second) { auto [conflictIt, success] = group->relaxedConflictingAtoms.try_emplace(neighbor.first, neighbor.second); From a3358edee22e0853b9b49a75e61f662c2efb9071 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:33:42 +0100 Subject: [PATCH 076/119] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20suggestions=20fr?= =?UTF-8?q?om=20code=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoned/layout_synthesizer/placer/HeuristicPlacer.hpp | 3 +-- .../zoned/layout_synthesizer/placer/HeuristicPlacer.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 9115a7729..ed9bfebca 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -641,8 +641,7 @@ class HeuristicPlacer : public PlacerBase { } else { assert(trials > 0); if (!queue.empty()) { - SPDLOG_TRACE("No neighbors found, restart search with priority {}", - goal->priority); + SPDLOG_TRACE("No neighbors found, restarting search"); currentItem = std::move(queue.top()); queue.pop(); } else { diff --git a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp index e6c59fd02..518f31c5a 100644 --- a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp +++ b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp @@ -642,6 +642,9 @@ auto HeuristicPlacer::placeGatesInEntanglementZone( scaleFactors, node); }, config_.maxNodes); + break; + default: + qc::unreachable(); } //===------------------------------------------------------------------===// // Extract the final mapping @@ -1055,6 +1058,9 @@ auto HeuristicPlacer::placeAtomsInStorageZone( scaleFactors, node); }, config_.maxNodes); + break; + default: + qc::unreachable(); } //===------------------------------------------------------------------===// // Extract the final mapping @@ -1282,7 +1288,7 @@ auto HeuristicPlacer::getNeighbors(const std::vector& gateJobs, child.groups, child.maxDistancesOfPlacedAtomsPerGroup); child.lookaheadCost += lookaheadCost; // add the final child to the list of children to be returned - neighbors.emplace_back(std::make_shared(std::move(child))); + neighbors.emplace_back(std::make_shared(std::move(child))); } return neighbors; } From 24cd390ef8ad14c2ae4b6b0d6d7e566670da9474 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:15:01 +0100 Subject: [PATCH 077/119] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20suggestions=20fr?= =?UTF-8?q?om=20code=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placer/HeuristicPlacer.hpp | 21 ++++++++++++++++--- .../placer/HeuristicPlacer.cpp | 20 ++++++++++++++++++ test/na/zoned/test_compiler.cpp | 3 ++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index ed9bfebca..31370e574 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -126,14 +126,14 @@ class HeuristicPlacer : 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 = method == Method::IDS ? 0.01F : 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 = method == Method::IDS ? 0.0F : 0.2F; + float deepeningValue = 0.0F; /** * @brief The cost function can consider the distance of atoms to their * interaction partner in the next layer. @@ -142,7 +142,7 @@ class HeuristicPlacer : 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 = method == Method::IDS ? 0.4F : 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 @@ -170,6 +170,21 @@ class HeuristicPlacer : public PlacerBase { * @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 + */ + [[nodiscard]] static auto createForMethod(Method method) -> Config; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( Config, useWindow, windowMinWidth, windowRatio, windowShare, method, deepeningFactor, deepeningValue, lookaheadFactor, reuseLevel, maxNodes, diff --git a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp index 518f31c5a..135e193f4 100644 --- a/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp +++ b/src/na/zoned/layout_synthesizer/placer/HeuristicPlacer.cpp @@ -1371,6 +1371,26 @@ auto HeuristicPlacer::checkCompatibilityAndAddPlacement( return false; } +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) { diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index b4d839f2e..cc852904c 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -479,7 +479,8 @@ TEST(FastRoutingAwareCompilerTest, ImprovedLoadingStoring) { Architecture::fromJSONString(architectureSpecificationGraphstate); const RoutingAwareCompiler::Config config = { .layoutSynthesizerConfig = - {.placerConfig = {.method = HeuristicPlacer::Config::Method::IDS}, + {.placerConfig = HeuristicPlacer::Config::createForMethod( + HeuristicPlacer::Config::Method::IDS), .routerConfig = {.method = IndependentSetRouter::Config::Method::RELAXED}}, .codeGeneratorConfig = {.warnUnsupportedGates = false}}; From bd38d30182cd6f300d9c7b0f83afc23a686ea142 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:21:58 +0100 Subject: [PATCH 078/119] =?UTF-8?q?=F0=9F=8E=A8=20Add=20evaluation=20scrip?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/eval_fast_relaxed_compiler.py | 1049 +++++++++++++++++++ 1 file changed, 1049 insertions(+) create mode 100755 eval/na/zoned/eval_fast_relaxed_compiler.py diff --git a/eval/na/zoned/eval_fast_relaxed_compiler.py b/eval/na/zoned/eval_fast_relaxed_compiler.py new file mode 100755 index 000000000..429f00fbd --- /dev/null +++ b/eval/na/zoned/eval_fast_relaxed_compiler.py @@ -0,0 +1,1049 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Script for evaluating the fast relaxed compiler.""" + +from __future__ import annotations + +import json +import pathlib +import queue +import re +from math import exp, sqrt +from multiprocessing import get_context +from typing import TYPE_CHECKING + +from mqt.bench import BenchmarkLevel, get_benchmark +from mqt.core import load +from qiskit import QuantumCircuit, transpile + +from mqt.qmap.na.zoned import ( + FastRelaxedRoutingAwareCompiler, + FastRoutingAwareCompiler, + RoutingAwareCompiler, + ZonedNeutralAtomArchitecture, +) + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator, Mapping + from multiprocessing import Queue + from typing import Any, ParamSpec, TypeVar + + from mqt.core.ir import QuantumComputation + + T = TypeVar("T", bound=FastRelaxedRoutingAwareCompiler | RoutingAwareCompiler | FastRoutingAwareCompiler) + P = ParamSpec("P") + R = ParamSpec("R") + + +def _proc_target(q: Queue, func: Callable[P, R], args: P.args, kwargs: P.kwargs) -> None: + """Target function for the process to run the given function and put the result in the queue. + + Args: + q: The queue to put the result in. + func: The function to run. + args: The positional arguments to pass to the function. + kwargs: The keyword arguments to pass to the function. + """ + try: + q.put(("ok", func(*args, **kwargs))) + except Exception as e: + q.put(("err", e)) + + +def run_with_process_timeout(func: Callable[P, R], timeout: float, *args: P.args, **kwargs: P.kwargs) -> R: + """Run a function in a separate process and timeout after the given timeout. + + Args: + func: The function to run. + timeout: The timeout in seconds. + *args: The positional arguments to pass to the function. + **kwargs: The keyword arguments to pass to the function. + + Returns: + The result of the function. + + Raises: + TimeoutError: If the function times out. + Exception: If the function raises an exception. + """ + ctx = get_context("fork") # use fork so bound methods don't need to be pickled on macOS/Unix + q = ctx.Queue() + p = ctx.Process(target=_proc_target, args=(q, func, args, kwargs)) + p.start() + try: + status, payload = q.get(block=True, timeout=timeout) + except queue.Empty as e: + msg = f"Timed out after {timeout}s" + raise TimeoutError(msg) from e + finally: + if p.is_alive(): + p.terminate() + p.join(2) + if p.is_alive(): + p.kill() + p.join() + if status == "ok": + return payload + if ( + status == "err" + and isinstance(payload, Exception) + and payload.args + and isinstance(payload.args[0], str) + and "Maximum number of nodes reached" in payload.args[0] + ): + msg = "Out of memory" + raise MemoryError(msg) + raise payload + + +def transpile_benchmark(benchmark: str, circuit: QuantumCircuit) -> QuantumCircuit: + """Transpile the given benchmark circuit to the native gate set. + + Args: + benchmark: Name of the benchmark. + circuit: The benchmark circuit to transpile. + + Returns: + The transpiled benchmark circuit. + """ + print(f"\033[32m[INFO]\033[0m Transpiling {benchmark}...") + flattened = QuantumCircuit(circuit.num_qubits, circuit.num_clbits) + flattened.compose(circuit, inplace=True) + transpiled = transpile( + flattened, basis_gates=["cz", "id", "u2", "u1", "u3"], optimization_level=3, seed_transpiler=0 + ) + stripped = QuantumCircuit(*transpiled.qregs, *transpiled.cregs) + for instr in transpiled.data: + if instr.operation.name not in {"measure", "barrier"}: + stripped.append(instr) + print("\033[32m[INFO]\033[0m Done") + return stripped + + +def benchmarks( + benchmark_dict: Iterable[tuple[str, tuple[BenchmarkLevel, Iterable[int]]]], +) -> Iterator[tuple[str, QuantumComputation]]: + """Yields the benchmark names and their circuits.""" + for benchmark, settings in benchmark_dict: + mode, limits = settings + for qubits in limits: + print(f"\033[32m[INFO]\033[0m Creating {benchmark} with {qubits} qubits...") + circuit = get_benchmark(benchmark, mode, qubits) + print("\033[32m[INFO]\033[0m Done") + transpiled = transpile_benchmark(benchmark, circuit) + qc = load(transpiled) + yield benchmark, qc + + +def _compile_wrapper( + compiler: FastRelaxedRoutingAwareCompiler | RoutingAwareCompiler | FastRoutingAwareCompiler, qc: QuantumComputation +) -> tuple[str, Mapping[str, Any]]: + """Compile and return the compiled code and stats. + + Args: + compiler: The compiler to use. + qc: The circuit to compile. + + Returns: + The compiled code and stats. + """ + return compiler.compile(qc), compiler.stats() + + +def process_benchmark( + compiler: FastRelaxedRoutingAwareCompiler | RoutingAwareCompiler | FastRoutingAwareCompiler, + setting_name: str, + qc: QuantumComputation, + benchmark_name: str, + evaluator: Evaluator, + use_cached: bool = False, +) -> bool: + """Compile and evaluate the given benchmark circuit. + + Args: + compiler: The compiler to use. + setting_name: Name of the compiler setting. + qc: The benchmark circuit to compile. + benchmark_name: Name of the benchmark. + evaluator: The evaluator to use. + use_cached: Whether to use the cached results. + """ + compiler_name = type(compiler).__name__ + if not use_cached: + print(f"\033[32m[INFO]\033[0m Compiling {benchmark_name} with {qc.num_qubits} qubits with {compiler_name}...") + try: + code, stats = run_with_process_timeout(_compile_wrapper, TIMEOUT, compiler, qc) + except TimeoutError as e: + print(f"\033[31m[ERROR]\033[0m Failed ({e})") + evaluator.print_timeout(benchmark_name, qc, compiler_name, setting_name) + return False + except MemoryError as e: + print(f"\033[31m[ERROR]\033[0m Failed ({e})") + evaluator.print_memout(benchmark_name, qc, compiler_name, setting_name) + return False + except RuntimeError as e: + print(f"\033[31m[ERROR]\033[0m Failed ({e})") + return False + + code = "\n".join(line for line in code.splitlines() if not line.startswith("@+ u")) + pathlib.Path(f"out/{compiler_name}/{setting_name}").mkdir(exist_ok=True, parents=True) + with pathlib.Path(f"out/{compiler_name}/{setting_name}/{benchmark_name}_{qc.num_qubits}.naviz").open( + "w", encoding="utf-8" + ) as f: + f.write(code) + print("\033[32m[INFO]\033[0m Done") + else: + print( + f"\033[32m[INFO]\033[0m Reading cached {benchmark_name} with {qc.num_qubits} qubits with {compiler_name}..." + ) + code = "" + path = pathlib.Path(f"out/{compiler_name}/{setting_name}/{benchmark_name}_{qc.num_qubits}.naviz") + if not path.exists(): + evaluator.print_memout(benchmark_name, qc, compiler_name, setting_name) + print("\033[31m[ERROR]\033[0m Abort") + return False + with path.open("r", encoding="utf-8") as f: + code = f.read() + stats = { + "schedulingTime": 0.0, + "reuseAnalysisTime": 0.0, + "layoutSynthesizerStatistics": { + "placementTime": 0.0, + "routingTime": 0.0, + }, + "codeGenerationTime": 0.0, + "totalTime": 0.0, + } + print("\033[32m[INFO]\033[0m Done") + + print(f"\033[32m[INFO]\033[0m Evaluating {benchmark_name} with {qc.num_qubits} qubits...") + evaluator.reset() + evaluator.evaluate(benchmark_name, compiler_name, qc, setting_name, code, stats) + evaluator.print_data() + print("\033[32m[INFO]\033[0m Done") + return True + + +class Evaluator: + """Class for evaluating compiled circuits. + + Attributes: + arch: The architecture dictionary. + filename: The output CSV filename. + circuit_name: Name of the circuit. + compiler: Name of the compiler. + setting: Compiler setting name. + one_qubit_gates: Number of one-qubit gates. + two_qubit_gates: Number of two-qubit gates. + scheduling_time: Time taken for scheduling. + reuse_analysis_time: Time taken for reuse analysis. + placement_time: Time taken for placement. + routing_time: Time taken for routing. + code_generation_time: Time taken for code generation. + total_time: Total compilation time. + one_qubit_gate_fidelity: Fidelity of one-qubit gates. + two_qubit_gate_fidelity: Fidelity of two-qubit gates. + transfer_fidelity: Fidelity of atom transfer operations. + rearrangement_fidelity: Fidelity of rearrangement operations. + coherence_fidelity: Fidelity due to coherence. + circuit_duration: Total duration of the circuit. + rearrangement_duration: Duration of rearrangement operations. + two_qubit_gate_layer: Number of two-qubit gate layers. + mean_two_qubit_gates: Mean number of two-qubit gates per layer. + rearrangement_layer: Number of rearrangement layers. + rearrangement_steps: Number of rearrangement steps. + min_two_qubit_gates: Minimum number of two-qubit gates in a layer. + max_two_qubit_gates: Maximum number of two-qubit gates in a layer. + sum_two_qubit_gates: Sum of two-qubit gates across layers. + last_op_is_shuttling: Flag indicating if the last operation was shuttling. + last_op_is_store: Flag indicating if the last operation was a store operation. + atom_locations: Dictionary of atom locations. + atom_busy_times: Dictionary of atom busy times. + atom_busy_rearrangement_times: Dictionary of atom busy times during rearrangement. + """ + + def __init__(self, arch: Mapping[str, Any], filename: str) -> None: + """Initialize the Evaluator. + + Args: + arch: The architecture dictionary. + filename: The output CSV filename. + """ + self.arch = arch + self.filename = filename + + self.circuit_name = "" + self.num_qubits = 0 + self.compiler = "" + self.setting = "" + self.one_qubit_gates = 0 + self.two_qubit_gates = 0 + + self.scheduling_time = 0 + self.reuse_analysis_time = 0 + self.placement_time = 0 + self.routing_time = 0 + self.code_generation_time = 0 + self.total_time = 0 + + self.one_qubit_gate_fidelity = 1.0 + self.two_qubit_gate_fidelity = 1.0 + self.transfer_fidelity = 1.0 + self.rearrangement_fidelity = 1.0 + self.coherence_fidelity = 1.0 + + self.circuit_duration = 0.0 + self.rearrangement_duration = 0.0 + + self.two_qubit_gate_layer = 0 + self.mean_two_qubit_gates = 0.0 + self.rearrangement_layer = 0 + self.rearrangement_steps = 0 + self.rearrangement_distance = 0 + self.num_loads = 0 + self.num_stores = 0 + + self.min_two_qubit_gates = None + self.max_two_qubit_gates = None + self.sum_two_qubit_gates = 0 + + self.last_op_is_shuttling = False + self.last_op_is_store = False + + self.atom_locations = {} + self.atom_busy_times = {} + self.atom_busy_rearrangement_times = {} + + def reset(self) -> None: + """Reset the Evaluator.""" + self.circuit_name = "" + self.num_qubits = 0 + self.compiler = "" + self.setting = "" + self.one_qubit_gates = 0 + self.two_qubit_gates = 0 + + self.scheduling_time = 0 + self.reuse_analysis_time = 0 + self.placement_time = 0 + self.routing_time = 0 + self.code_generation_time = 0 + self.total_time = 0 + + self.one_qubit_gate_fidelity = 1.0 + self.two_qubit_gate_fidelity = 1.0 + self.transfer_fidelity = 1.0 + self.rearrangement_fidelity = 1.0 + self.coherence_fidelity = 1.0 + + self.circuit_duration = 0.0 + self.rearrangement_duration = 0.0 + + self.two_qubit_gate_layer = 0 + self.mean_two_qubit_gates = 0.0 + self.rearrangement_layer = 0 + self.rearrangement_steps = 0 + self.rearrangement_distance = 0 + self.num_loads = 0 + self.num_stores = 0 + + self.min_two_qubit_gates = None + self.max_two_qubit_gates = None + self.sum_two_qubit_gates = 0 + + self.last_op_is_shuttling = False + self.last_op_is_store = False + + self.atom_locations = {} + self.atom_busy_times = {} + self.atom_busy_rearrangement_times = {} + + def _process_load(self, line: str, it: Iterator[str]) -> None: + """Process a load operation. + + Args: + line: The current line being processed. + it: An iterator over the remaining lines. + """ + # Extract atoms from the load operation + atoms = [] + match = re.match(r"@\+ load \[", line) + if match: + # Multi-line load + for next_line in it: + next_line_stripped = next_line.strip() + if next_line_stripped == "]": + break + assert next_line_stripped in self.atom_locations, ( + f"Atom {next_line_stripped} not found in atom locations" + ) + atoms.append(next_line_stripped) + else: + # Single atom load + match = re.match(r"@\+ load (\w+)", line) + if match: + assert match.group(1) in self.atom_locations, f"Atom {match.group(1)} not found in atom locations" + atoms.append(match.group(1)) + self._apply_load(atoms) + + def _process_move(self, line: str, it: Iterator[str]) -> None: + """Process a move operation. + + Args: + line: The current line being processed. + it: An iterator over the remaining lines. + """ + # Extract atoms and coordinates from the move operation + moves = [] + match = re.match(r"@\+ move \[", line) + if match: + # Multi-line move + for next_line in it: + next_line_stripped = next_line.strip() + if next_line_stripped == "]": + break + move_match = re.match(r"\((-?\d+\.\d+), (-?\d+\.\d+)\) (\w+)", next_line_stripped) + if move_match: + x, y, atom = move_match.groups() + assert atom in self.atom_locations, f"Atom {atom} not found in atom locations" + moves.append((atom, (int(float(x)), int(float(y))))) + else: + # Single atom move + match = re.match(r"@\+ move \((-?\d+\.\d+), (-?\d+\.\d+)\) (\w+)", line) + if match: + x, y, atom = match.groups() + assert atom in self.atom_locations, f"Atom {atom} not found in atom locations" + moves.append((atom, (int(float(x)), int(float(y))))) + self._apply_move(moves) + + def _process_store(self, line: str, it: Iterator[str]) -> None: + """Process a store operation. + + Args: + line: The current line being processed. + it: An iterator over the remaining lines. + """ + # Extract atoms from the store operation + match = re.match(r"@\+ store \[", line) + atoms = [] + if match: + # Multi-line store + for next_line in it: + next_line_stripped = next_line.strip() + if next_line_stripped == "]": + break + assert next_line_stripped in self.atom_locations, ( + f"Atom {next_line_stripped} not found in atom locations" + ) + atoms.append(next_line_stripped) + else: + # Single atom store + match = re.match(r"@\+ store (\w+)", line) + if match: + assert match.group(1) in self.atom_locations, f"Atom {match.group(1)} not found in atom locations" + atoms.append(match.group(1)) + self._apply_store(atoms) + + def _process_cz(self) -> None: + """Process a cz operation.""" + atoms = [] + y_min = self.arch["entanglement_zones"][0]["slms"][0]["location"][1] + for atom, coord in self.atom_locations.items(): + if coord[1] >= y_min: # atom is in the entanglement zone + atoms.append(atom) + assert len(atoms) % 2 == 0, f"Expected even number of atoms in entanglement zone, got {len(atoms)}" + self._apply_cz(atoms) + + def _process_u(self, line: str, it: Iterator[str]) -> None: + """Process a u operation. + + Args: + line: The current line being processed. + it: An iterator over the remaining lines. + """ + # Extract atoms from u operation + atoms = [] + match = re.match(r"@\+ u( \d\.\d+){3} \[", line) + if match: + # Multi-line u + for next_line in it: + next_line_stripped = next_line.strip() + if next_line_stripped == "]": + break + assert next_line_stripped in self.atom_locations, ( + f"Atom {next_line_stripped} not found in atom locations" + ) + atoms.append(next_line_stripped) + else: + # Single atom u + match = re.match(r"@\+ u( \d\.\d+){3} (\w+)", line) + if match: + if match.group(2) not in self.atom_locations: + self._apply_global_u() + return + atoms.append(match.group(2)) + self._apply_u(atoms) + + def _process_rz(self, line: str, it: Iterator[str]) -> None: + """Process a rz operation. + + Args: + line: The current line being processed. + it: An iterator over the remaining lines. + """ + # Extract atoms from u operation + atoms = [] + match = re.match(r"@\+ rz \d\.\d+ \[", line) + if match: + # Multi-line u + for next_line in it: + next_line_stripped = next_line.strip() + if next_line_stripped == "]": + break + assert next_line_stripped in self.atom_locations, ( + f"Atom {next_line_stripped} not found in atom locations" + ) + atoms.append(next_line_stripped) + else: + # Single atom u + match = re.match(r"@\+ rz \d\.\d+ (\w+)", line) + if match: + assert match.group(1) in self.atom_locations, f"Atom {match.group(1)} not found in atom locations" + atoms.append(match.group(1)) + self._apply_rz(atoms) + + def _apply_load(self, atoms: list[str]) -> None: + """Apply a load operation. + + Args: + atoms: List of atoms to load. + """ + if not self.last_op_is_shuttling: + self.rearrangement_layer += 1 + self.last_op_is_shuttling = True + self.last_op_is_store = False + + self.num_loads += 1 + self.transfer_fidelity *= self.arch["operation_fidelity"]["atom_transfer"] ** len(atoms) + self.circuit_duration += self.arch["operation_duration"]["atom_transfer"] + self.rearrangement_duration += self.arch["operation_duration"]["atom_transfer"] + for atom in atoms: + self.atom_busy_times[atom] += self.arch["operation_duration"]["atom_transfer"] + self.atom_busy_rearrangement_times[atom] += self.arch["operation_duration"]["atom_transfer"] + + def _apply_move(self, moves: list[tuple[str, tuple[int, int]]]) -> None: + """Apply a move operation. + + Args: + moves: List of tuples containing atom names and their target coordinates. + """ + self.last_op_is_shuttling = True + # do not change this value to ignore intermediate moves between store operations + # self.last_op_is_store = False + + max_distance = 0.0 + for atom, coord in moves: + if atom in self.atom_locations: + distance = sqrt( + (coord[0] - self.atom_locations[atom][0]) ** 2 + (coord[1] - self.atom_locations[atom][1]) ** 2 + ) + self.rearrangement_distance += distance + max_distance = max(max_distance, distance) + + t_d_max = 200 + d_max = 110 + jerk = 32 * d_max / t_d_max**3 # 0.00044 + v_max = d_max / t_d_max * 2 # 1.1 + + if max_distance <= d_max: + rearrangement_time = 2 * (4 * max_distance / jerk) ** (1 / 3) + else: + rearrangement_time = t_d_max + (max_distance - d_max) / v_max + self.circuit_duration += rearrangement_time + self.rearrangement_duration += rearrangement_time + # Update atom locations + for atom, coord in moves: + assert atom in self.atom_locations, f"Atom {atom} not found in atom locations" + self.atom_locations[atom] = coord + + def _apply_store(self, atoms: list[str]) -> None: + """Apply a store operation. + + Args: + atoms: List of atoms to store. + """ + if not self.last_op_is_store: + self.rearrangement_steps += 1 + self.last_op_is_shuttling = True + self.last_op_is_store = True + + self.num_stores += 1 + self.transfer_fidelity *= self.arch["operation_fidelity"]["atom_transfer"] ** len(atoms) + self.circuit_duration += self.arch["operation_duration"]["atom_transfer"] + self.rearrangement_duration += self.arch["operation_duration"]["atom_transfer"] + for atom in atoms: + self.atom_busy_times[atom] += self.arch["operation_duration"]["atom_transfer"] + self.atom_busy_rearrangement_times[atom] += self.arch["operation_duration"]["atom_transfer"] + + def _apply_cz(self, atoms: list[str]) -> None: + """Apply a cz operation. + + Args: + atoms: List of atoms involved in the cz operation. + """ + self.last_op_is_shuttling = False + self.last_op_is_store = False + + # assuming the compiler works correctly, if there are n atoms in the + # entanglement zone, n/2 gates are executed and there is no idling atom + self.two_qubit_gate_fidelity *= self.arch["operation_fidelity"]["rydberg_gate"] ** (len(atoms) / 2) + + self.circuit_duration += self.arch["operation_duration"]["rydberg_gate"] + for atom in atoms: + self.atom_busy_times[atom] += self.arch["operation_duration"]["single_qubit_gate"] + + self.two_qubit_gate_layer += 1 + self.min_two_qubit_gates = ( + min(self.min_two_qubit_gates, len(atoms) / 2) if self.min_two_qubit_gates else len(atoms) / 2 + ) + self.sum_two_qubit_gates += len(atoms) / 2 + self.max_two_qubit_gates = ( + max(self.max_two_qubit_gates, len(atoms) / 2) if self.max_two_qubit_gates else len(atoms) / 2 + ) + + def _apply_u(self, atoms: list[str]) -> None: + """Apply a u operation. + + Args: + atoms: List of atoms involved in the u operation. + """ + self.last_op_is_shuttling = False + self.last_op_is_store = False + + self.one_qubit_gate_fidelity *= self.arch["operation_fidelity"]["single_qubit_gate"] ** len(atoms) + + self.circuit_duration += self.arch["operation_duration"]["single_qubit_gate"] + for atom in atoms: + self.atom_busy_times[atom] += self.arch["operation_duration"]["single_qubit_gate"] + + def _apply_global_u(self) -> None: + """Apply a global u operation.""" + self.last_op_is_shuttling = False + self.last_op_is_store = False + + self.one_qubit_gate_fidelity *= self.arch["operation_fidelity"]["single_qubit_gate"] ** len(self.atom_locations) + + self.circuit_duration += self.arch["operation_duration"]["single_qubit_gate"] + for atom in self.atom_locations: + self.atom_busy_times[atom] += self.arch["operation_duration"]["single_qubit_gate"] + + def _apply_global_ry(self) -> None: + """Apply a global rydberg gate operation.""" + self._apply_global_u() + + def _apply_rz(self, atoms: list[str]) -> None: + """Apply a rz operation. + + Args: + atoms: List of atoms involved in the rz operation. + """ + self._apply_u(atoms) + + def evaluate( + self, name: str, compiler: str, qc: QuantumComputation, setting: str, code: str, stats: Mapping[str, Any] + ) -> None: + """Evaluate a circuit. + + Args: + name: Name of the circuit. + compiler: Name of the compiler. + qc: The quantum circuit. + setting: Compiler setting name. + code: The compiled code. + stats: Compilation statistics. + """ + self.circuit_name = name + self.num_qubits = qc.num_qubits + self.compiler = compiler + self.setting = setting + self.one_qubit_gates = sum(len(op.get_used_qubits()) == 1 for op in qc) + self.two_qubit_gates = sum(len(op.get_used_qubits()) == 2 for op in qc) + + self.scheduling_time = stats["schedulingTime"] + self.reuse_analysis_time = stats["reuseAnalysisTime"] + self.placement_time = stats["layoutSynthesizerStatistics"]["placementTime"] + self.routing_time = stats["layoutSynthesizerStatistics"]["routingTime"] + self.code_generation_time = stats["codeGenerationTime"] + self.total_time = stats["totalTime"] + + it = iter(code.splitlines()) + + for line in it: + match = re.match(r"atom\s+\((-?\d+\.\d+),\s*(-?\d+\.\d+)\)\s+(\w+)", line) + if match: + x, y, atom_name = match.groups() + self.atom_locations[atom_name] = (int(float(x)), int(float(y))) + else: + # put line back on top of iterator + it = iter([line, *list(it)]) + break + + self.atom_busy_times = dict.fromkeys(self.atom_locations.keys(), 0.0) + self.atom_busy_rearrangement_times = dict.fromkeys(self.atom_locations.keys(), 0.0) + + for line in it: + if line.startswith("@+ load"): + self._process_load(line, it) + elif line.startswith("@+ move"): + self._process_move(line, it) + elif line.startswith("@+ store"): + self._process_store(line, it) + elif line.startswith("@+ cz"): + self._process_cz() + elif line.startswith("@+ u"): + self._process_u(line, it) + else: + assert line.startswith("@+ rz"), f"Unrecognized operation: {line}" + self._process_rz(line, it) + + # in contrast to the transfer fidelity, this fidelity includes decoherence + # during rearrangement + self.rearrangement_fidelity = self.transfer_fidelity + for busy_time in self.atom_busy_rearrangement_times.values(): + idle_time = self.rearrangement_duration - busy_time + t_eff = self.arch["qubit_spec"]["T"] + self.rearrangement_fidelity *= exp(-idle_time / t_eff) + self.coherence_fidelity = 1.0 + for busy_time in self.atom_busy_times.values(): + idle_time = self.circuit_duration - busy_time + t_eff = self.arch["qubit_spec"]["T"] + self.coherence_fidelity *= exp(-idle_time / t_eff) + self.mean_two_qubit_gates = self.sum_two_qubit_gates / self.two_qubit_gate_layer + + def print_header(self) -> None: + """Print the header of the CSV file.""" + with pathlib.Path(self.filename).open("w", encoding="utf-8") as csv: + csv.write( + "circuit_name,num_qubits,compiler,setting,status,one_qubit_gates,two_qubit_gates,scheduling_time," + "reuse_analysis_time,placement_time,routing_time,code_generation_time,total_time,two_qubit_gate_layer," + "min_two_qubit_gates,mean_two_qubit_gates,max_two_qubit_gates,one_qubit_gate_fidelity," + "two_qubit_gate_fidelity,transfer_fidelity,coherence_fidelity,total_fidelity," + "rearrangement_layer,rearrangement_steps,rearrangement_duration,rearrangement_fidelity," + "circuit_duration,num_loads,num_stores,rearrangement_distance\n" + ) + + def print_data(self) -> None: + """Print the data of the CSV file.""" + total_fidelity = ( + self.one_qubit_gate_fidelity + * self.two_qubit_gate_fidelity + * self.transfer_fidelity + * self.coherence_fidelity + ) + with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: + csv.write( + f"{self.circuit_name},{self.num_qubits},{self.compiler},{self.setting},ok,{self.one_qubit_gates}," + f"{self.two_qubit_gates},{self.scheduling_time},{self.reuse_analysis_time},{self.placement_time}," + f"{self.routing_time},{self.code_generation_time},{self.total_time},{self.two_qubit_gate_layer}," + f"{self.min_two_qubit_gates},{self.mean_two_qubit_gates},{self.max_two_qubit_gates}," + f"{self.one_qubit_gate_fidelity},{self.two_qubit_gate_fidelity},{self.transfer_fidelity}," + f"{self.coherence_fidelity},{total_fidelity},{self.rearrangement_layer},{self.rearrangement_steps}," + f"{self.rearrangement_duration},{self.rearrangement_fidelity},{self.circuit_duration},{self.num_loads}," + f"{self.num_stores},{self.rearrangement_distance}\n" + ) + + def print_timeout(self, circuit_name: str, qc: QuantumComputation, compiler: str, setting: str) -> None: + """Print the data of the CSV file. + + Args: + circuit_name: Name of the circuit. + qc: The quantum circuit. + compiler: Name of the compiler. + setting: Compiler setting name. + """ + with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: + csv.write(f"{circuit_name},{qc.num_qubits},{compiler},{setting},timeout,,,,,,,,,,,,,,,,,,,,,,,,,\n") + + def print_memout(self, circuit_name: str, qc: QuantumComputation, compiler: str, setting: str) -> None: + """Print the data of the CSV file. + + Args: + circuit_name: Name of the circuit. + qc: The quantum circuit. + compiler: Name of the compiler. + setting: Compiler setting name. + """ + with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: + csv.write(f"{circuit_name},{qc.num_qubits},{compiler},{setting},memout,,,,,,,,,,,,,,,,,,,,,,,,,\n") + + +def main() -> None: + """Main function for evaluating the fast relaxed compiler.""" + print("\033[32m[INFO]\033[0m Reading in architecture...") + with pathlib.Path("square_architecture.json").open(encoding="utf-8") as f: + arch_dict = json.load(f) + arch = ZonedNeutralAtomArchitecture.from_json_file("square_architecture.json") + arch.to_namachine_file("arch.namachine") + print("\033[32m[INFO]\033[0m Done") + RoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.9, + deepening_value=8.0, + lookahead_factor=0.2, + reuse_level=5.0, + max_nodes=int(1e7), + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.9, + deepening_value=8.0, + lookahead_factor=0.2, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.02, + deepening_value=1.0, + lookahead_factor=0.4, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.01, + deepening_value=1.0, + lookahead_factor=0.1, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.02, + deepening_value=1.0, + lookahead_factor=0.1, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.02, + deepening_value=0.0, + lookahead_factor=0.1, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.0, + deepening_value=0.0, + lookahead_factor=0.2, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.02, + deepening_value=0.0, + lookahead_factor=0.5, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.0, + deepening_value=0.0, + lookahead_factor=0.4, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.02, + deepening_value=0.0, + lookahead_factor=0.45, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + fast_relaxed_compiler_less_split = FastRelaxedRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.01, + deepening_value=0.0, + lookahead_factor=0.4, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + prefer_split=0.0, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRelaxedRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.01, + deepening_value=0.0, + lookahead_factor=0.4, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + prefer_split=1.0, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRelaxedRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.01, + deepening_value=0.0, + lookahead_factor=0.4, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + prefer_split=2.0, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + FastRelaxedRoutingAwareCompiler( + arch, + use_window=True, + window_min_width=16, + window_ratio=1.0, + window_share=0.8, + deepening_factor=0.01, + deepening_value=0.0, + lookahead_factor=0.4, + reuse_level=5.0, + trials=TRIALS, + queue_capacity=100, + prefer_split=4.0, + log_level=LOG_LEVEL, + warn_unsupported_gates=False, + ) + evaluator = Evaluator(arch_dict, "results.csv") + # evaluator.print_header() + + # Seed graphstate with seed = 0 (!) + for benchmark, qc in benchmarks([ + ("graphstate", (BenchmarkLevel.INDEP, [60, 80, 100, 120, 140, 160, 180, 200, 500, 1000, 2000, 5000])), + ("qft", (BenchmarkLevel.INDEP, [500, 1000])), + ("qpeexact", (BenchmarkLevel.INDEP, [500, 1000])), + ("wstate", (BenchmarkLevel.INDEP, [500, 1000])), + ("qaoa", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), + ("vqe_two_local", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), + ]): + qc.qasm3(f"in/{benchmark}_n{qc.num_qubits}.qasm") + # process_benchmark(compiler_default, "default", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark(fast_compiler_default, "default", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark(fast_compiler_half_deepening, "double_deepening4", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark(fast_compiler_no_deepening, "half_lookahead6", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark( + # fast_compiler_double_lookahead, "half_lookahead5", qc, benchmark, evaluator, use_cached=CACHE + # ) + # process_benchmark( + # fast_compiler_half_lookahead, "half_lookahead4", qc, benchmark, evaluator, use_cached=CACHE + # ) + # process_benchmark(fast_compiler_zero_deep, "zero_deep", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark(fast_compiler_almost_zero_deep, "almost_zero_deep3", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark(fast_compiler_less_deep, "less_deep5", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark(fast_compiler_least_deep, "least_deep4", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark(fast_relaxed_compiler_default, "default3", qc, benchmark, evaluator, use_cached=CACHE) + process_benchmark(fast_relaxed_compiler_less_split, "no_split", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark(fast_relaxed_compiler_more_split, "more_split3", qc, benchmark, evaluator, use_cached=CACHE) + # process_benchmark( + # fast_relaxed_compiler_even_more_split, "even_more_split3", qc, benchmark, evaluator, use_cached=CACHE + # ) + + +LOG_LEVEL = "error" +TIMEOUT = 15 * 60 +TRIALS = 4 +CACHE = False + +if __name__ == "__main__": + main() From aa427111c3a143956172234f3526d00b30101d9f Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:23:26 +0100 Subject: [PATCH 079/119] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20evaluation=20sc?= =?UTF-8?q?ript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eval_fast_relaxed_compiler.py => eval_ids_relaxed_routing.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename eval/na/zoned/eval_fast_relaxed_compiler.py => eval_ids_relaxed_routing.py (100%) diff --git a/eval/na/zoned/eval_fast_relaxed_compiler.py b/eval_ids_relaxed_routing.py similarity index 100% rename from eval/na/zoned/eval_fast_relaxed_compiler.py rename to eval_ids_relaxed_routing.py From a9368b86f475c4ed2b188cc415fbe7a1b5ac0d80 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:26:14 +0100 Subject: [PATCH 080/119] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20evaluation=20sc?= =?UTF-8?q?ript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../na/zoned/eval_ids_relaxed_routing.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename eval_ids_relaxed_routing.py => eval/na/zoned/eval_ids_relaxed_routing.py (100%) diff --git a/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py similarity index 100% rename from eval_ids_relaxed_routing.py rename to eval/na/zoned/eval_ids_relaxed_routing.py From fd3d75ef5940052fc9f6a03aca3a8a9c4f7fb73f Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:54:04 +0100 Subject: [PATCH 081/119] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20leftovers=20fro?= =?UTF-8?q?m=20parking=5Foffset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 4 ---- test/na/zoned/test_code_generator.cpp | 1 - 2 files changed, 5 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 247302ec4..e5e9f5ac8 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -92,7 +92,6 @@ class RoutingAgnosticCompiler: use_window: bool = ..., window_size: int = ..., dynamic_placement: bool = ..., - parking_offset: int = ..., warn_unsupported_gates: bool = ..., ) -> None: """Create a routing-agnostic compiler for the given architecture and configurations. @@ -106,7 +105,6 @@ class RoutingAgnosticCompiler: use_window: whether to use a window for the placer window_size: the size of the window for the placer dynamic_placement: whether to use dynamic placement for the placer - parking_offset: the parking offset of the code generator warn_unsupported_gates: whether to warn about unsupported gates in the code generator """ @classmethod @@ -158,7 +156,6 @@ class RoutingAwareCompiler: lookahead_factor: float = ..., reuse_level: float = ..., max_nodes: int = ..., - parking_offset: int = ..., warn_unsupported_gates: bool = ..., ) -> None: """Create a routing-aware compiler for the given architecture and configurations. @@ -196,7 +193,6 @@ 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. - parking_offset: is the parking offset of the code generator warn_unsupported_gates: is a flag whether to warn about unsupported gates in the code generator """ diff --git a/test/na/zoned/test_code_generator.cpp b/test/na/zoned/test_code_generator.cpp index 433116b6d..4cd3cf137 100644 --- a/test/na/zoned/test_code_generator.cpp +++ b/test/na/zoned/test_code_generator.cpp @@ -46,7 +46,6 @@ constexpr std::string_view architectureJson = R"({ "rydberg_range": [[[5, 70], [55, 110]]] })"; constexpr std::string_view configJson = R"({ - "parkingOffset" : 1, "warnUnsupportedGates" : true })"; class CodeGeneratorGenerateTest : public ::testing::Test { From 8bb70ea51015e73b6da56871cdc47a4a99986855 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:01:39 +0100 Subject: [PATCH 082/119] =?UTF-8?q?=F0=9F=8E=A8=20Expose=20trials=20and=20?= =?UTF-8?q?queueCapacity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 11 +++++++++-- python/mqt/qmap/na/zoned.pyi | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 331c4e79d..67352acfe 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -157,7 +157,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { const na::zoned::HeuristicPlacer::Config::Method placementMethod, const float deepeningFactor, const float deepeningValue, const float lookaheadFactor, const float reuseLevel, - const size_t maxNodes, const bool warnUnsupportedGates) + const size_t maxNodes, const size_t trials, + const size_t queueCapacity, const bool warnUnsupportedGates) -> na::zoned::RoutingAwareCompiler { na::zoned::RoutingAwareCompiler::Config config; config.logLevel = spdlog::level::from_str(logLevel); @@ -174,7 +175,10 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { .deepeningValue = deepeningValue, .lookaheadFactor = lookaheadFactor, .reuseLevel = reuseLevel, - .maxNodes = maxNodes}; + .maxNodes = maxNodes, + .trials = trials, + .queueCapacity = queueCapacity, + }; config.codeGeneratorConfig = {.warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; @@ -204,6 +208,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, + "queueCapacity"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.queueCapacity, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index e5e9f5ac8..d33e854bc 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -193,6 +193,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. warn_unsupported_gates: is a flag whether to warn about unsupported gates in the code generator """ From c6267a96671606f7915a3474dc0149ab3d07c83c Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:07:46 +0100 Subject: [PATCH 083/119] =?UTF-8?q?=F0=9F=8E=A8=20Expose=20prefer=5Fsplit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 38 +++++++++++++++++++++--------------- python/mqt/qmap/na/zoned.pyi | 5 +++-- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 67352acfe..6f089c7fa 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -84,20 +84,21 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { routingAgnosticCompiler.def( py::init([](const na::zoned::Architecture& arch, const std::string& logLevel, const double maxFillingFactor, - const na::zoned::IndependentSetRouter::Config::Method - routingMethod, const bool useWindow, const size_t windowSize, const bool dynamicPlacement, - const bool warnUnsupportedGates) + const na::zoned::IndependentSetRouter::Config::Method + routingMethod, + const double preferSplit, const bool warnUnsupportedGates) -> na::zoned::RoutingAgnosticCompiler { na::zoned::RoutingAgnosticCompiler::Config config; config.logLevel = spdlog::level::from_str(logLevel); config.schedulerConfig.maxFillingFactor = maxFillingFactor; - config.layoutSynthesizerConfig.routerConfig.method = routingMethod; config.layoutSynthesizerConfig.placerConfig = { .useWindow = useWindow, .windowSize = windowSize, .dynamicPlacement = dynamicPlacement}; + config.layoutSynthesizerConfig.routerConfig = { + .method = routingMethod, .preferSplit = preferSplit}; config.codeGeneratorConfig = {.warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; @@ -105,14 +106,16 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { 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, - "routing_method"_a = - defaultConfig.layoutSynthesizerConfig.routerConfig.method, "use_window"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.useWindow, "window_size"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.windowSize, "dynamic_placement"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.dynamicPlacement, + "routing_method"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.method, + "prefer_split"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.preferSplit, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } @@ -149,22 +152,21 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { routingAwareCompiler.def( py::init( [](const na::zoned::Architecture& arch, const std::string& logLevel, - const double maxFillingFactor, - const na::zoned::IndependentSetRouter::Config::Method - routingMethod, - const bool useWindow, const size_t windowMinWidth, - const double windowRatio, const double windowShare, + 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 bool warnUnsupportedGates) + 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.routerConfig.method = - routingMethod; config.layoutSynthesizerConfig.placerConfig = { .useWindow = useWindow, .windowMinWidth = windowMinWidth, @@ -179,6 +181,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { .trials = trials, .queueCapacity = queueCapacity, }; + config.layoutSynthesizerConfig.routerConfig = { + .method = routingMethod, .preferSplit = preferSplit}; config.codeGeneratorConfig = {.warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; @@ -186,8 +190,6 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { 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, - "routing_method"_a = - defaultConfig.layoutSynthesizerConfig.routerConfig.method, "use_window"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.useWindow, "window_min_width"_a = @@ -211,6 +213,10 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { "trials"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.trials, "queueCapacity"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.queueCapacity, + "routing_method"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.method, + "prefer_split"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.preferSplit, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index d33e854bc..3c332d34c 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -167,8 +167,6 @@ class RoutingAwareCompiler: max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel - routing_method: is the routing method that should be used for the - independent set router use_window: is a flag whether to use a window for the placer window_min_width: is the minimum width of the window for the placer window_ratio: is the ratio between the height and the width of the window @@ -195,6 +193,9 @@ class RoutingAwareCompiler: about 6 GB plus the size of the rest of the data structures. trials: is the number of restarts during IDS. queue_capacity: is the maximum capacity of the priority queue used during IDS. + routing_method: is the routing method that should be used for the + independent set router + prefer_split: is the threshold factor for group merging decisions during routing. warn_unsupported_gates: is a flag whether to warn about unsupported gates in the code generator """ From e0ef278ea94962e9e56e4c972801d16c2ba3b70f Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Wed, 10 Dec 2025 08:05:36 +0100 Subject: [PATCH 084/119] =?UTF-8?q?=F0=9F=8E=A8=20Clean-up=20eval=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/eval_ids_relaxed_routing.py | 503 +++------------------- 1 file changed, 65 insertions(+), 438 deletions(-) diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index 429f00fbd..ecbe37ff9 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -15,7 +15,7 @@ import pathlib import queue import re -from math import exp, sqrt +from math import sqrt from multiprocessing import get_context from typing import TYPE_CHECKING @@ -23,21 +23,15 @@ from mqt.core import load from qiskit import QuantumCircuit, transpile -from mqt.qmap.na.zoned import ( - FastRelaxedRoutingAwareCompiler, - FastRoutingAwareCompiler, - RoutingAwareCompiler, - ZonedNeutralAtomArchitecture, -) +from mqt.qmap.na.zoned import PlacementMethod, RoutingAwareCompiler, RoutingMethod, ZonedNeutralAtomArchitecture if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator, Mapping from multiprocessing import Queue - from typing import Any, ParamSpec, TypeVar + from typing import Any, ParamSpec from mqt.core.ir import QuantumComputation - T = TypeVar("T", bound=FastRelaxedRoutingAwareCompiler | RoutingAwareCompiler | FastRoutingAwareCompiler) P = ParamSpec("P") R = ParamSpec("R") @@ -134,17 +128,13 @@ def benchmarks( for benchmark, settings in benchmark_dict: mode, limits = settings for qubits in limits: - print(f"\033[32m[INFO]\033[0m Creating {benchmark} with {qubits} qubits...") circuit = get_benchmark(benchmark, mode, qubits) - print("\033[32m[INFO]\033[0m Done") transpiled = transpile_benchmark(benchmark, circuit) qc = load(transpiled) yield benchmark, qc -def _compile_wrapper( - compiler: FastRelaxedRoutingAwareCompiler | RoutingAwareCompiler | FastRoutingAwareCompiler, qc: QuantumComputation -) -> tuple[str, Mapping[str, Any]]: +def _compile_wrapper(compiler: RoutingAwareCompiler, qc: QuantumComputation) -> tuple[str, Mapping[str, Any]]: """Compile and return the compiled code and stats. Args: @@ -158,12 +148,11 @@ def _compile_wrapper( def process_benchmark( - compiler: FastRelaxedRoutingAwareCompiler | RoutingAwareCompiler | FastRoutingAwareCompiler, + compiler: RoutingAwareCompiler, setting_name: str, qc: QuantumComputation, benchmark_name: str, evaluator: Evaluator, - use_cached: bool = False, ) -> bool: """Compile and evaluate the given benchmark circuit. @@ -173,59 +162,34 @@ def process_benchmark( qc: The benchmark circuit to compile. benchmark_name: Name of the benchmark. evaluator: The evaluator to use. - use_cached: Whether to use the cached results. """ compiler_name = type(compiler).__name__ - if not use_cached: - print(f"\033[32m[INFO]\033[0m Compiling {benchmark_name} with {qc.num_qubits} qubits with {compiler_name}...") - try: - code, stats = run_with_process_timeout(_compile_wrapper, TIMEOUT, compiler, qc) - except TimeoutError as e: - print(f"\033[31m[ERROR]\033[0m Failed ({e})") - evaluator.print_timeout(benchmark_name, qc, compiler_name, setting_name) - return False - except MemoryError as e: - print(f"\033[31m[ERROR]\033[0m Failed ({e})") - evaluator.print_memout(benchmark_name, qc, compiler_name, setting_name) - return False - except RuntimeError as e: - print(f"\033[31m[ERROR]\033[0m Failed ({e})") - return False - - code = "\n".join(line for line in code.splitlines() if not line.startswith("@+ u")) - pathlib.Path(f"out/{compiler_name}/{setting_name}").mkdir(exist_ok=True, parents=True) - with pathlib.Path(f"out/{compiler_name}/{setting_name}/{benchmark_name}_{qc.num_qubits}.naviz").open( - "w", encoding="utf-8" - ) as f: - f.write(code) - print("\033[32m[INFO]\033[0m Done") - else: - print( - f"\033[32m[INFO]\033[0m Reading cached {benchmark_name} with {qc.num_qubits} qubits with {compiler_name}..." - ) - code = "" - path = pathlib.Path(f"out/{compiler_name}/{setting_name}/{benchmark_name}_{qc.num_qubits}.naviz") - if not path.exists(): - evaluator.print_memout(benchmark_name, qc, compiler_name, setting_name) - print("\033[31m[ERROR]\033[0m Abort") - return False - with path.open("r", encoding="utf-8") as f: - code = f.read() - stats = { - "schedulingTime": 0.0, - "reuseAnalysisTime": 0.0, - "layoutSynthesizerStatistics": { - "placementTime": 0.0, - "routingTime": 0.0, - }, - "codeGenerationTime": 0.0, - "totalTime": 0.0, - } - print("\033[32m[INFO]\033[0m Done") + print(f"\033[32m[INFO]\033[0m Compiling {benchmark_name} with {qc.num_qubits} qubits with {compiler_name}...") + try: + code, stats = run_with_process_timeout(_compile_wrapper, TIMEOUT, compiler, qc) + except TimeoutError as e: + print(f"\033[31m[ERROR]\033[0m Failed ({e})") + evaluator.print_timeout(benchmark_name, qc, compiler_name, setting_name) + return False + except MemoryError as e: + print(f"\033[31m[ERROR]\033[0m Failed ({e})") + evaluator.print_memout(benchmark_name, qc, compiler_name, setting_name) + return False + except RuntimeError as e: + print(f"\033[31m[ERROR]\033[0m Failed ({e})") + return False + + code = "\n".join(line for line in code.splitlines() if not line.startswith("@+ u")) + pathlib.Path(f"out/{compiler_name}/{setting_name}").mkdir(exist_ok=True, parents=True) + with pathlib.Path(f"out/{compiler_name}/{setting_name}/{benchmark_name}_{qc.num_qubits}.naviz").open( + "w", encoding="utf-8" + ) as f: + f.write(code) + print("\033[32m[INFO]\033[0m Done") print(f"\033[32m[INFO]\033[0m Evaluating {benchmark_name} with {qc.num_qubits} qubits...") evaluator.reset() - evaluator.evaluate(benchmark_name, compiler_name, qc, setting_name, code, stats) + evaluator.evaluate(benchmark_name, qc, setting_name, code, stats) evaluator.print_data() print("\033[32m[INFO]\033[0m Done") return True @@ -238,9 +202,8 @@ class Evaluator: arch: The architecture dictionary. filename: The output CSV filename. circuit_name: Name of the circuit. - compiler: Name of the compiler. setting: Compiler setting name. - one_qubit_gates: Number of one-qubit gates. + num_qubits: Number of qubits. two_qubit_gates: Number of two-qubit gates. scheduling_time: Time taken for scheduling. reuse_analysis_time: Time taken for reuse analysis. @@ -248,25 +211,10 @@ class Evaluator: routing_time: Time taken for routing. code_generation_time: Time taken for code generation. total_time: Total compilation time. - one_qubit_gate_fidelity: Fidelity of one-qubit gates. - two_qubit_gate_fidelity: Fidelity of two-qubit gates. - transfer_fidelity: Fidelity of atom transfer operations. - rearrangement_fidelity: Fidelity of rearrangement operations. - coherence_fidelity: Fidelity due to coherence. - circuit_duration: Total duration of the circuit. rearrangement_duration: Duration of rearrangement operations. two_qubit_gate_layer: Number of two-qubit gate layers. - mean_two_qubit_gates: Mean number of two-qubit gates per layer. - rearrangement_layer: Number of rearrangement layers. - rearrangement_steps: Number of rearrangement steps. - min_two_qubit_gates: Minimum number of two-qubit gates in a layer. max_two_qubit_gates: Maximum number of two-qubit gates in a layer. - sum_two_qubit_gates: Sum of two-qubit gates across layers. - last_op_is_shuttling: Flag indicating if the last operation was shuttling. - last_op_is_store: Flag indicating if the last operation was a store operation. atom_locations: Dictionary of atom locations. - atom_busy_times: Dictionary of atom busy times. - atom_busy_rearrangement_times: Dictionary of atom busy times during rearrangement. """ def __init__(self, arch: Mapping[str, Any], filename: str) -> None: @@ -281,9 +229,7 @@ def __init__(self, arch: Mapping[str, Any], filename: str) -> None: self.circuit_name = "" self.num_qubits = 0 - self.compiler = "" self.setting = "" - self.one_qubit_gates = 0 self.two_qubit_gates = 0 self.scheduling_time = 0 @@ -293,41 +239,17 @@ def __init__(self, arch: Mapping[str, Any], filename: str) -> None: self.code_generation_time = 0 self.total_time = 0 - self.one_qubit_gate_fidelity = 1.0 - self.two_qubit_gate_fidelity = 1.0 - self.transfer_fidelity = 1.0 - self.rearrangement_fidelity = 1.0 - self.coherence_fidelity = 1.0 - - self.circuit_duration = 0.0 self.rearrangement_duration = 0.0 - self.two_qubit_gate_layer = 0 - self.mean_two_qubit_gates = 0.0 - self.rearrangement_layer = 0 - self.rearrangement_steps = 0 - self.rearrangement_distance = 0 - self.num_loads = 0 - self.num_stores = 0 - - self.min_two_qubit_gates = None self.max_two_qubit_gates = None - self.sum_two_qubit_gates = 0 - - self.last_op_is_shuttling = False - self.last_op_is_store = False self.atom_locations = {} - self.atom_busy_times = {} - self.atom_busy_rearrangement_times = {} def reset(self) -> None: """Reset the Evaluator.""" self.circuit_name = "" self.num_qubits = 0 - self.compiler = "" self.setting = "" - self.one_qubit_gates = 0 self.two_qubit_gates = 0 self.scheduling_time = 0 @@ -337,33 +259,11 @@ def reset(self) -> None: self.code_generation_time = 0 self.total_time = 0 - self.one_qubit_gate_fidelity = 1.0 - self.two_qubit_gate_fidelity = 1.0 - self.transfer_fidelity = 1.0 - self.rearrangement_fidelity = 1.0 - self.coherence_fidelity = 1.0 - - self.circuit_duration = 0.0 self.rearrangement_duration = 0.0 - self.two_qubit_gate_layer = 0 - self.mean_two_qubit_gates = 0.0 - self.rearrangement_layer = 0 - self.rearrangement_steps = 0 - self.rearrangement_distance = 0 - self.num_loads = 0 - self.num_stores = 0 - - self.min_two_qubit_gates = None self.max_two_qubit_gates = None - self.sum_two_qubit_gates = 0 - - self.last_op_is_shuttling = False - self.last_op_is_store = False self.atom_locations = {} - self.atom_busy_times = {} - self.atom_busy_rearrangement_times = {} def _process_load(self, line: str, it: Iterator[str]) -> None: """Process a load operation. @@ -519,24 +419,13 @@ def _process_rz(self, line: str, it: Iterator[str]) -> None: atoms.append(match.group(1)) self._apply_rz(atoms) - def _apply_load(self, atoms: list[str]) -> None: + def _apply_load(self, _: list[str]) -> None: """Apply a load operation. Args: - atoms: List of atoms to load. + _: List of atoms to load. """ - if not self.last_op_is_shuttling: - self.rearrangement_layer += 1 - self.last_op_is_shuttling = True - self.last_op_is_store = False - - self.num_loads += 1 - self.transfer_fidelity *= self.arch["operation_fidelity"]["atom_transfer"] ** len(atoms) - self.circuit_duration += self.arch["operation_duration"]["atom_transfer"] self.rearrangement_duration += self.arch["operation_duration"]["atom_transfer"] - for atom in atoms: - self.atom_busy_times[atom] += self.arch["operation_duration"]["atom_transfer"] - self.atom_busy_rearrangement_times[atom] += self.arch["operation_duration"]["atom_transfer"] def _apply_move(self, moves: list[tuple[str, tuple[int, int]]]) -> None: """Apply a move operation. @@ -544,17 +433,12 @@ def _apply_move(self, moves: list[tuple[str, tuple[int, int]]]) -> None: Args: moves: List of tuples containing atom names and their target coordinates. """ - self.last_op_is_shuttling = True - # do not change this value to ignore intermediate moves between store operations - # self.last_op_is_store = False - max_distance = 0.0 for atom, coord in moves: if atom in self.atom_locations: distance = sqrt( (coord[0] - self.atom_locations[atom][0]) ** 2 + (coord[1] - self.atom_locations[atom][1]) ** 2 ) - self.rearrangement_distance += distance max_distance = max(max_distance, distance) t_d_max = 200 @@ -566,31 +450,19 @@ def _apply_move(self, moves: list[tuple[str, tuple[int, int]]]) -> None: rearrangement_time = 2 * (4 * max_distance / jerk) ** (1 / 3) else: rearrangement_time = t_d_max + (max_distance - d_max) / v_max - self.circuit_duration += rearrangement_time self.rearrangement_duration += rearrangement_time # Update atom locations for atom, coord in moves: assert atom in self.atom_locations, f"Atom {atom} not found in atom locations" self.atom_locations[atom] = coord - def _apply_store(self, atoms: list[str]) -> None: + def _apply_store(self, _: list[str]) -> None: """Apply a store operation. Args: - atoms: List of atoms to store. + _: List of atoms to store. """ - if not self.last_op_is_store: - self.rearrangement_steps += 1 - self.last_op_is_shuttling = True - self.last_op_is_store = True - - self.num_stores += 1 - self.transfer_fidelity *= self.arch["operation_fidelity"]["atom_transfer"] ** len(atoms) - self.circuit_duration += self.arch["operation_duration"]["atom_transfer"] self.rearrangement_duration += self.arch["operation_duration"]["atom_transfer"] - for atom in atoms: - self.atom_busy_times[atom] += self.arch["operation_duration"]["atom_transfer"] - self.atom_busy_rearrangement_times[atom] += self.arch["operation_duration"]["atom_transfer"] def _apply_cz(self, atoms: list[str]) -> None: """Apply a cz operation. @@ -598,51 +470,20 @@ def _apply_cz(self, atoms: list[str]) -> None: Args: atoms: List of atoms involved in the cz operation. """ - self.last_op_is_shuttling = False - self.last_op_is_store = False - - # assuming the compiler works correctly, if there are n atoms in the - # entanglement zone, n/2 gates are executed and there is no idling atom - self.two_qubit_gate_fidelity *= self.arch["operation_fidelity"]["rydberg_gate"] ** (len(atoms) / 2) - - self.circuit_duration += self.arch["operation_duration"]["rydberg_gate"] - for atom in atoms: - self.atom_busy_times[atom] += self.arch["operation_duration"]["single_qubit_gate"] - self.two_qubit_gate_layer += 1 - self.min_two_qubit_gates = ( - min(self.min_two_qubit_gates, len(atoms) / 2) if self.min_two_qubit_gates else len(atoms) / 2 - ) - self.sum_two_qubit_gates += len(atoms) / 2 self.max_two_qubit_gates = ( max(self.max_two_qubit_gates, len(atoms) / 2) if self.max_two_qubit_gates else len(atoms) / 2 ) def _apply_u(self, atoms: list[str]) -> None: - """Apply a u operation. + """Apply an u operation. Args: atoms: List of atoms involved in the u operation. """ - self.last_op_is_shuttling = False - self.last_op_is_store = False - - self.one_qubit_gate_fidelity *= self.arch["operation_fidelity"]["single_qubit_gate"] ** len(atoms) - - self.circuit_duration += self.arch["operation_duration"]["single_qubit_gate"] - for atom in atoms: - self.atom_busy_times[atom] += self.arch["operation_duration"]["single_qubit_gate"] def _apply_global_u(self) -> None: """Apply a global u operation.""" - self.last_op_is_shuttling = False - self.last_op_is_store = False - - self.one_qubit_gate_fidelity *= self.arch["operation_fidelity"]["single_qubit_gate"] ** len(self.atom_locations) - - self.circuit_duration += self.arch["operation_duration"]["single_qubit_gate"] - for atom in self.atom_locations: - self.atom_busy_times[atom] += self.arch["operation_duration"]["single_qubit_gate"] def _apply_global_ry(self) -> None: """Apply a global rydberg gate operation.""" @@ -656,14 +497,11 @@ def _apply_rz(self, atoms: list[str]) -> None: """ self._apply_u(atoms) - def evaluate( - self, name: str, compiler: str, qc: QuantumComputation, setting: str, code: str, stats: Mapping[str, Any] - ) -> None: + def evaluate(self, name: str, qc: QuantumComputation, setting: str, code: str, stats: Mapping[str, Any]) -> None: """Evaluate a circuit. Args: name: Name of the circuit. - compiler: Name of the compiler. qc: The quantum circuit. setting: Compiler setting name. code: The compiled code. @@ -671,9 +509,7 @@ def evaluate( """ self.circuit_name = name self.num_qubits = qc.num_qubits - self.compiler = compiler self.setting = setting - self.one_qubit_gates = sum(len(op.get_used_qubits()) == 1 for op in qc) self.two_qubit_gates = sum(len(op.get_used_qubits()) == 2 for op in qc) self.scheduling_time = stats["schedulingTime"] @@ -695,9 +531,6 @@ def evaluate( it = iter([line, *list(it)]) break - self.atom_busy_times = dict.fromkeys(self.atom_locations.keys(), 0.0) - self.atom_busy_rearrangement_times = dict.fromkeys(self.atom_locations.keys(), 0.0) - for line in it: if line.startswith("@+ load"): self._process_load(line, it) @@ -713,50 +546,23 @@ def evaluate( assert line.startswith("@+ rz"), f"Unrecognized operation: {line}" self._process_rz(line, it) - # in contrast to the transfer fidelity, this fidelity includes decoherence - # during rearrangement - self.rearrangement_fidelity = self.transfer_fidelity - for busy_time in self.atom_busy_rearrangement_times.values(): - idle_time = self.rearrangement_duration - busy_time - t_eff = self.arch["qubit_spec"]["T"] - self.rearrangement_fidelity *= exp(-idle_time / t_eff) - self.coherence_fidelity = 1.0 - for busy_time in self.atom_busy_times.values(): - idle_time = self.circuit_duration - busy_time - t_eff = self.arch["qubit_spec"]["T"] - self.coherence_fidelity *= exp(-idle_time / t_eff) - self.mean_two_qubit_gates = self.sum_two_qubit_gates / self.two_qubit_gate_layer - def print_header(self) -> None: """Print the header of the CSV file.""" with pathlib.Path(self.filename).open("w", encoding="utf-8") as csv: csv.write( - "circuit_name,num_qubits,compiler,setting,status,one_qubit_gates,two_qubit_gates,scheduling_time," - "reuse_analysis_time,placement_time,routing_time,code_generation_time,total_time,two_qubit_gate_layer," - "min_two_qubit_gates,mean_two_qubit_gates,max_two_qubit_gates,one_qubit_gate_fidelity," - "two_qubit_gate_fidelity,transfer_fidelity,coherence_fidelity,total_fidelity," - "rearrangement_layer,rearrangement_steps,rearrangement_duration,rearrangement_fidelity," - "circuit_duration,num_loads,num_stores,rearrangement_distance\n" + "circuit_name,num_qubits,setting,status,two_qubit_gates,scheduling_time,reuse_analysis_time," + "placement_time,routing_time,code_generation_time,total_time,two_qubit_gate_layer,max_two_qubit_gates," + "rearrangement_duration\n" ) def print_data(self) -> None: """Print the data of the CSV file.""" - total_fidelity = ( - self.one_qubit_gate_fidelity - * self.two_qubit_gate_fidelity - * self.transfer_fidelity - * self.coherence_fidelity - ) with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: csv.write( - f"{self.circuit_name},{self.num_qubits},{self.compiler},{self.setting},ok,{self.one_qubit_gates}," - f"{self.two_qubit_gates},{self.scheduling_time},{self.reuse_analysis_time},{self.placement_time}," + f"{self.circuit_name},{self.num_qubits},{self.setting},ok,{self.two_qubit_gates}," + f"{self.scheduling_time},{self.reuse_analysis_time},{self.placement_time}," f"{self.routing_time},{self.code_generation_time},{self.total_time},{self.two_qubit_gate_layer}," - f"{self.min_two_qubit_gates},{self.mean_two_qubit_gates},{self.max_two_qubit_gates}," - f"{self.one_qubit_gate_fidelity},{self.two_qubit_gate_fidelity},{self.transfer_fidelity}," - f"{self.coherence_fidelity},{total_fidelity},{self.rearrangement_layer},{self.rearrangement_steps}," - f"{self.rearrangement_duration},{self.rearrangement_fidelity},{self.circuit_duration},{self.num_loads}," - f"{self.num_stores},{self.rearrangement_distance}\n" + f"{self.max_two_qubit_gates},{self.rearrangement_duration}\n" ) def print_timeout(self, circuit_name: str, qc: QuantumComputation, compiler: str, setting: str) -> None: @@ -792,223 +598,64 @@ def main() -> None: arch = ZonedNeutralAtomArchitecture.from_json_file("square_architecture.json") arch.to_namachine_file("arch.namachine") print("\033[32m[INFO]\033[0m Done") - RoutingAwareCompiler( + astar_compiler = RoutingAwareCompiler( arch, + log_level="error", + max_filling_factor=0.8, use_window=True, window_min_width=16, window_ratio=1.0, window_share=0.8, + placement_method=PlacementMethod.astar, deepening_factor=0.9, deepening_value=8.0, lookahead_factor=0.2, reuse_level=5.0, max_nodes=int(1e7), - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.9, - deepening_value=8.0, - lookahead_factor=0.2, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.02, - deepening_value=1.0, - lookahead_factor=0.4, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.01, - deepening_value=1.0, - lookahead_factor=0.1, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.02, - deepening_value=1.0, - lookahead_factor=0.1, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.02, - deepening_value=0.0, - lookahead_factor=0.1, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.0, - deepening_value=0.0, - lookahead_factor=0.2, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.02, - deepening_value=0.0, - lookahead_factor=0.5, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.0, - deepening_value=0.0, - lookahead_factor=0.4, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, + routing_method=RoutingMethod.strict, warn_unsupported_gates=False, ) - FastRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.02, - deepening_value=0.0, - lookahead_factor=0.45, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - fast_relaxed_compiler_less_split = FastRelaxedRoutingAwareCompiler( + ids_compiler = RoutingAwareCompiler( arch, + log_level="error", + max_filling_factor=0.8, use_window=True, window_min_width=16, window_ratio=1.0, window_share=0.8, + placement_method=PlacementMethod.ids, deepening_factor=0.01, deepening_value=0.0, lookahead_factor=0.4, reuse_level=5.0, - trials=TRIALS, + trials=4, queue_capacity=100, - prefer_split=0.0, - log_level=LOG_LEVEL, + routing_method=RoutingMethod.strict, warn_unsupported_gates=False, ) - FastRelaxedRoutingAwareCompiler( + relaxed_compiler = RoutingAwareCompiler( arch, + log_level="error", + max_filling_factor=0.8, use_window=True, window_min_width=16, window_ratio=1.0, window_share=0.8, + placement_method=PlacementMethod.ids, deepening_factor=0.01, deepening_value=0.0, lookahead_factor=0.4, reuse_level=5.0, - trials=TRIALS, + trials=4, queue_capacity=100, + routing_method=RoutingMethod.relaxed, prefer_split=1.0, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRelaxedRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.01, - deepening_value=0.0, - lookahead_factor=0.4, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - prefer_split=2.0, - log_level=LOG_LEVEL, - warn_unsupported_gates=False, - ) - FastRelaxedRoutingAwareCompiler( - arch, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - deepening_factor=0.01, - deepening_value=0.0, - lookahead_factor=0.4, - reuse_level=5.0, - trials=TRIALS, - queue_capacity=100, - prefer_split=4.0, - log_level=LOG_LEVEL, warn_unsupported_gates=False, ) + evaluator = Evaluator(arch_dict, "results.csv") - # evaluator.print_header() + evaluator.print_header() - # Seed graphstate with seed = 0 (!) for benchmark, qc in benchmarks([ ("graphstate", (BenchmarkLevel.INDEP, [60, 80, 100, 120, 140, 160, 180, 200, 500, 1000, 2000, 5000])), ("qft", (BenchmarkLevel.INDEP, [500, 1000])), @@ -1018,32 +665,12 @@ def main() -> None: ("vqe_two_local", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), ]): qc.qasm3(f"in/{benchmark}_n{qc.num_qubits}.qasm") - # process_benchmark(compiler_default, "default", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark(fast_compiler_default, "default", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark(fast_compiler_half_deepening, "double_deepening4", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark(fast_compiler_no_deepening, "half_lookahead6", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark( - # fast_compiler_double_lookahead, "half_lookahead5", qc, benchmark, evaluator, use_cached=CACHE - # ) - # process_benchmark( - # fast_compiler_half_lookahead, "half_lookahead4", qc, benchmark, evaluator, use_cached=CACHE - # ) - # process_benchmark(fast_compiler_zero_deep, "zero_deep", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark(fast_compiler_almost_zero_deep, "almost_zero_deep3", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark(fast_compiler_less_deep, "less_deep5", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark(fast_compiler_least_deep, "least_deep4", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark(fast_relaxed_compiler_default, "default3", qc, benchmark, evaluator, use_cached=CACHE) - process_benchmark(fast_relaxed_compiler_less_split, "no_split", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark(fast_relaxed_compiler_more_split, "more_split3", qc, benchmark, evaluator, use_cached=CACHE) - # process_benchmark( - # fast_relaxed_compiler_even_more_split, "even_more_split3", qc, benchmark, evaluator, use_cached=CACHE - # ) - - -LOG_LEVEL = "error" + process_benchmark(astar_compiler, "astar", qc, benchmark, evaluator) + process_benchmark(ids_compiler, "ids", qc, benchmark, evaluator) + process_benchmark(relaxed_compiler, "relaxed", qc, benchmark, evaluator) + + TIMEOUT = 15 * 60 -TRIALS = 4 -CACHE = False if __name__ == "__main__": main() From 37006fa38329f98dc91e3cabcc5bf03ef5878cfb Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:54:04 +0100 Subject: [PATCH 085/119] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20leftovers=20fro?= =?UTF-8?q?m=20parking=5Foffset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 4 ---- test/na/zoned/test_code_generator.cpp | 1 - 2 files changed, 5 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 414a72e94..495564e78 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -80,7 +80,6 @@ class RoutingAgnosticCompiler: use_window: bool = ..., window_size: int = ..., dynamic_placement: bool = ..., - parking_offset: int = ..., warn_unsupported_gates: bool = ..., ) -> None: """Create a routing-agnostic compiler for the given architecture and configurations. @@ -94,7 +93,6 @@ class RoutingAgnosticCompiler: use_window: whether to use a window for the placer window_size: the size of the window for the placer dynamic_placement: whether to use dynamic placement for the placer - parking_offset: the parking offset of the code generator warn_unsupported_gates: whether to warn about unsupported gates in the code generator """ @classmethod @@ -145,7 +143,6 @@ class RoutingAwareCompiler: lookahead_factor: float = ..., reuse_level: float = ..., max_nodes: int = ..., - parking_offset: int = ..., warn_unsupported_gates: bool = ..., ) -> None: """Create a routing-aware compiler for the given architecture and configurations. @@ -181,7 +178,6 @@ 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. - parking_offset: is the parking offset of the code generator warn_unsupported_gates: is a flag whether to warn about unsupported gates in the code generator """ diff --git a/test/na/zoned/test_code_generator.cpp b/test/na/zoned/test_code_generator.cpp index 433116b6d..4cd3cf137 100644 --- a/test/na/zoned/test_code_generator.cpp +++ b/test/na/zoned/test_code_generator.cpp @@ -46,7 +46,6 @@ constexpr std::string_view architectureJson = R"({ "rydberg_range": [[[5, 70], [55, 110]]] })"; constexpr std::string_view configJson = R"({ - "parkingOffset" : 1, "warnUnsupportedGates" : true })"; class CodeGeneratorGenerateTest : public ::testing::Test { From 122fe456eba949dc9e19861399b3500e7c2f30f5 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:07:46 +0100 Subject: [PATCH 086/119] =?UTF-8?q?=F0=9F=8E=A8=20Expose=20prefer=5Fsplit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 34 +++++++++++++++++++++------------- python/mqt/qmap/na/zoned.pyi | 5 +++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index cbee0a64f..0b53e3d1f 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -74,20 +74,21 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { routingAgnosticCompiler.def( py::init([](const na::zoned::Architecture& arch, const std::string& logLevel, const double maxFillingFactor, - const na::zoned::IndependentSetRouter::Config::Method - routingMethod, const bool useWindow, const size_t windowSize, const bool dynamicPlacement, - const bool warnUnsupportedGates) + const na::zoned::IndependentSetRouter::Config::Method + routingMethod, + const double preferSplit, const bool warnUnsupportedGates) -> na::zoned::RoutingAgnosticCompiler { na::zoned::RoutingAgnosticCompiler::Config config; config.logLevel = spdlog::level::from_str(logLevel); config.schedulerConfig.maxFillingFactor = maxFillingFactor; - config.layoutSynthesizerConfig.routerConfig.method = routingMethod; config.layoutSynthesizerConfig.placerConfig = { .useWindow = useWindow, .windowSize = windowSize, .dynamicPlacement = dynamicPlacement}; + config.layoutSynthesizerConfig.routerConfig = { + .method = routingMethod, .preferSplit = preferSplit}; config.codeGeneratorConfig = {.warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; @@ -95,14 +96,16 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { 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, - "routing_method"_a = - defaultConfig.layoutSynthesizerConfig.routerConfig.method, "use_window"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.useWindow, "window_size"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.windowSize, "dynamic_placement"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.dynamicPlacement, + "routing_method"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.method, + "prefer_split"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.preferSplit, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } @@ -139,18 +142,18 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { routingAwareCompiler.def( py::init([](const na::zoned::Architecture& arch, const std::string& logLevel, const double maxFillingFactor, - const na::zoned::IndependentSetRouter::Config::Method - routingMethod, 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 bool warnUnsupportedGates) + 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.routerConfig.method = routingMethod; config.layoutSynthesizerConfig.placerConfig = { .useWindow = useWindow, .windowMinWidth = windowMinWidth, @@ -160,7 +163,10 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { .deepeningValue = deepeningValue, .lookaheadFactor = lookaheadFactor, .reuseLevel = reuseLevel, - .maxNodes = maxNodes}; + .maxNodes = maxNodes, + }; + config.layoutSynthesizerConfig.routerConfig = { + .method = routingMethod, .preferSplit = preferSplit}; config.codeGeneratorConfig = {.warnUnsupportedGates = warnUnsupportedGates}; return {arch, config}; @@ -168,8 +174,6 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { 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, - "routing_method"_a = - defaultConfig.layoutSynthesizerConfig.routerConfig.method, "use_window"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.useWindow, "window_min_width"_a = @@ -188,6 +192,10 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { defaultConfig.layoutSynthesizerConfig.placerConfig.reuseLevel, "max_nodes"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.maxNodes, + "routing_method"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.method, + "prefer_split"_a = + defaultConfig.layoutSynthesizerConfig.routerConfig.preferSplit, "warn_unsupported_gates"_a = defaultConfig.codeGeneratorConfig.warnUnsupportedGates); } diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 495564e78..6ff9c79e8 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -154,8 +154,6 @@ class RoutingAwareCompiler: max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel - routing_method: is the routing method that should be used for the - independent set router use_window: is a flag whether to use a window for the placer window_min_width: is the minimum width of the window for the placer window_ratio: is the ratio between the height and the width of the window @@ -178,6 +176,9 @@ 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. + routing_method: is the routing method that should be used for the + independent set router + prefer_split: is the threshold factor for group merging decisions during routing. warn_unsupported_gates: is a flag whether to warn about unsupported gates in the code generator """ From 9a447cb7efc2863a474c942919aa85c48474b742 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:01:39 +0100 Subject: [PATCH 087/119] =?UTF-8?q?=F0=9F=8E=A8=20Expose=20trials=20and=20?= =?UTF-8?q?queueCapacity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 8 +++++++- python/mqt/qmap/na/zoned.pyi | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 125839c6a..094895ebb 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -158,7 +158,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { 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 maxNodes, const size_t trials, + const size_t queueCapacity, const na::zoned::IndependentSetRouter::Config::Method routingMethod, const double preferSplit, const bool warnUnsupportedGates) @@ -178,6 +179,8 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { .lookaheadFactor = lookaheadFactor, .reuseLevel = reuseLevel, .maxNodes = maxNodes, + .trials = trials, + .queueCapacity = queueCapacity, }; config.layoutSynthesizerConfig.routerConfig = { .method = routingMethod, .preferSplit = preferSplit}; @@ -208,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, + "queueCapacity"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.queueCapacity, "routing_method"_a = defaultConfig.layoutSynthesizerConfig.routerConfig.method, "prefer_split"_a = diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index bb6e38809..3c332d34c 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -191,6 +191,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. From 57e8dcfc2eb097dc5e6cc2233bf4fb47e65642c7 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Wed, 10 Dec 2025 08:43:25 +0100 Subject: [PATCH 088/119] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20pyi=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/eval_ids_relaxed_routing.py | 22 +++++++++++++--------- python/mqt/qmap/na/zoned.pyi | 19 ++++++++++++++----- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index ecbe37ff9..f7ae23f6d 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -12,6 +12,7 @@ from __future__ import annotations import json +import os import pathlib import queue import re @@ -601,7 +602,7 @@ def main() -> None: astar_compiler = RoutingAwareCompiler( arch, log_level="error", - max_filling_factor=0.8, + max_filling_factor=0.9, use_window=True, window_min_width=16, window_ratio=1.0, @@ -618,7 +619,7 @@ def main() -> None: ids_compiler = RoutingAwareCompiler( arch, log_level="error", - max_filling_factor=0.8, + max_filling_factor=0.9, use_window=True, window_min_width=16, window_ratio=1.0, @@ -636,7 +637,7 @@ def main() -> None: relaxed_compiler = RoutingAwareCompiler( arch, log_level="error", - max_filling_factor=0.8, + max_filling_factor=0.9, use_window=True, window_min_width=16, window_ratio=1.0, @@ -657,12 +658,13 @@ def main() -> None: evaluator.print_header() for benchmark, qc in benchmarks([ - ("graphstate", (BenchmarkLevel.INDEP, [60, 80, 100, 120, 140, 160, 180, 200, 500, 1000, 2000, 5000])), - ("qft", (BenchmarkLevel.INDEP, [500, 1000])), - ("qpeexact", (BenchmarkLevel.INDEP, [500, 1000])), - ("wstate", (BenchmarkLevel.INDEP, [500, 1000])), - ("qaoa", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), - ("vqe_two_local", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), + ("graphstate", (BenchmarkLevel.INDEP, [60])), + # ("graphstate", (BenchmarkLevel.INDEP, [60, 80, 100, 120, 140, 160, 180, 200, 500, 1000, 2000, 5000])), + # ("qft", (BenchmarkLevel.INDEP, [500, 1000])), + # ("qpeexact", (BenchmarkLevel.INDEP, [500, 1000])), + # ("wstate", (BenchmarkLevel.INDEP, [500, 1000])), + # ("qaoa", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), + # ("vqe_two_local", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), ]): qc.qasm3(f"in/{benchmark}_n{qc.num_qubits}.qasm") process_benchmark(astar_compiler, "astar", qc, benchmark, evaluator) @@ -673,4 +675,6 @@ def main() -> None: TIMEOUT = 15 * 60 if __name__ == "__main__": + # set working directory to script location + os.chdir(pathlib.Path(pathlib.Path(__file__).resolve()).parent) main() diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 3c332d34c..3655e82cf 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -88,10 +88,11 @@ class RoutingAgnosticCompiler: arch: ZonedNeutralAtomArchitecture, log_level: str = ..., max_filling_factor: float = ..., - routing_method: RoutingMethod = ..., use_window: bool = ..., window_size: int = ..., dynamic_placement: bool = ..., + routing_method: RoutingMethod = ..., + prefer_split: float = ..., warn_unsupported_gates: bool = ..., ) -> None: """Create a routing-agnostic compiler for the given architecture and configurations. @@ -100,12 +101,17 @@ class RoutingAgnosticCompiler: arch: is the zoned neutral atom architecture log_level: is the log level for the compiler, possible values are "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel - routing_method: is the routing method that should be used for the independent set router + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., + it sets the limit for the maximum number of entangling gates that are + scheduled in parallel use_window: whether to use a window for the placer window_size: the size of the window for the placer dynamic_placement: whether to use dynamic placement for the placer - warn_unsupported_gates: whether to warn about unsupported gates in the code generator + routing_method: is the routing method that should be used for the + independent set router + prefer_split: is the threshold factor for group merging decisions during routing. + warn_unsupported_gates: whether to warn about unsupported gates in the code + generator """ @classmethod def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAgnosticCompiler: @@ -145,7 +151,6 @@ class RoutingAwareCompiler: arch: ZonedNeutralAtomArchitecture, log_level: str = ..., max_filling_factor: float = ..., - routing_method: RoutingMethod = ..., use_window: bool = ..., window_min_width: int = ..., window_ratio: float = ..., @@ -156,6 +161,10 @@ class RoutingAwareCompiler: lookahead_factor: float = ..., reuse_level: float = ..., max_nodes: int = ..., + trials: int = ..., + queue_capacity: int = ..., + routing_method: RoutingMethod = ..., + prefer_split: float = ..., warn_unsupported_gates: bool = ..., ) -> None: """Create a routing-aware compiler for the given architecture and configurations. From ff1bb53c4d9c0d48cb000cdde86b2463526e0607 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:10:54 +0100 Subject: [PATCH 089/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20pyi=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 6ff9c79e8..977fe0996 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -76,10 +76,11 @@ class RoutingAgnosticCompiler: arch: ZonedNeutralAtomArchitecture, log_level: str = ..., max_filling_factor: float = ..., - routing_method: RoutingMethod = ..., use_window: bool = ..., window_size: int = ..., dynamic_placement: bool = ..., + routing_method: RoutingMethod = ..., + prefer_split: float = ..., warn_unsupported_gates: bool = ..., ) -> None: """Create a routing-agnostic compiler for the given architecture and configurations. @@ -88,12 +89,17 @@ class RoutingAgnosticCompiler: arch: is the zoned neutral atom architecture log_level: is the log level for the compiler, possible values are "debug", "info", "warning", "error", "critical" - max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel - routing_method: is the routing method that should be used for the independent set router + max_filling_factor: is the maximum filling factor for the entanglement zone, i.e., + it sets the limit for the maximum number of entangling gates that are + scheduled in parallel use_window: whether to use a window for the placer window_size: the size of the window for the placer dynamic_placement: whether to use dynamic placement for the placer - warn_unsupported_gates: whether to warn about unsupported gates in the code generator + routing_method: is the routing method that should be used for the + independent set router + prefer_split: is the threshold factor for group merging decisions during routing. + warn_unsupported_gates: whether to warn about unsupported gates in the code + generator """ @classmethod def from_json_string(cls, arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAgnosticCompiler: @@ -133,7 +139,6 @@ class RoutingAwareCompiler: arch: ZonedNeutralAtomArchitecture, log_level: str = ..., max_filling_factor: float = ..., - routing_method: RoutingMethod = ..., use_window: bool = ..., window_min_width: int = ..., window_ratio: float = ..., @@ -143,6 +148,8 @@ class RoutingAwareCompiler: lookahead_factor: float = ..., reuse_level: float = ..., max_nodes: int = ..., + routing_method: RoutingMethod = ..., + prefer_split: float = ..., warn_unsupported_gates: bool = ..., ) -> None: """Create a routing-aware compiler for the given architecture and configurations. From aeb3d9f712316aedcc7bd8569be2d97dfac88572 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:16:49 +0100 Subject: [PATCH 090/119] =?UTF-8?q?=E2=9E=95=20Add=20architecture=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/square_architecture.json | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 eval/na/zoned/square_architecture.json diff --git a/eval/na/zoned/square_architecture.json b/eval/na/zoned/square_architecture.json new file mode 100644 index 000000000..20e376415 --- /dev/null +++ b/eval/na/zoned/square_architecture.json @@ -0,0 +1,73 @@ +{ + "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": [4, 4], + "r": 73, + "c": 101, + "location": [0, 0] + } + ], + "offset": [0, 0], + "dimension": [400, 288] + } + ], + "entanglement_zones": [ + { + "zone_id": 0, + "slms": [ + { + "id": 1, + "site_separation": [12, 10], + "r": 10, + "c": 34, + "location": [1, 309] + }, + { + "id": 2, + "site_separation": [12, 10], + "r": 10, + "c": 34, + "location": [3, 309] + } + ], + "offset": [1, 309], + "dimension": [398, 90] + } + ], + "aods": [ + { + "id": 0, + "site_separation": 2, + "r": 100, + "c": 100 + } + ], + "arch_range": [ + [0, 0], + [400, 400] + ], + "rydberg_range": [ + [ + [0, 308], + [400, 400] + ] + ] +} From 22a58b295ed009e529a514414e7ff111e3782617 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:43:38 +0100 Subject: [PATCH 091/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20queue=20capacity?= =?UTF-8?q?=20parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 6f089c7fa..1c169b043 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -211,7 +211,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { "max_nodes"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.maxNodes, "trials"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.trials, - "queueCapacity"_a = + "queue_capacity"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.queueCapacity, "routing_method"_a = defaultConfig.layoutSynthesizerConfig.routerConfig.method, From ab6e623a2d63104da2937e49d2a69989c0829830 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:43:38 +0100 Subject: [PATCH 092/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20queue=20capacity?= =?UTF-8?q?=20parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 094895ebb..86ba2bdbc 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -212,7 +212,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { "max_nodes"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.maxNodes, "trials"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.trials, - "queueCapacity"_a = + "queue_capacity"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.queueCapacity, "routing_method"_a = defaultConfig.layoutSynthesizerConfig.routerConfig.method, From ef7216f3ed7b31906e1883a563e077edf57f7212 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:07:08 +0100 Subject: [PATCH 093/119] =?UTF-8?q?=F0=9F=8E=A8=20Update=20evaluation=20sc?= =?UTF-8?q?ript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/eval_ids_relaxed_routing.py | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index f7ae23f6d..559c4a6ba 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -658,19 +658,34 @@ def main() -> None: evaluator.print_header() for benchmark, qc in benchmarks([ - ("graphstate", (BenchmarkLevel.INDEP, [60])), - # ("graphstate", (BenchmarkLevel.INDEP, [60, 80, 100, 120, 140, 160, 180, 200, 500, 1000, 2000, 5000])), - # ("qft", (BenchmarkLevel.INDEP, [500, 1000])), - # ("qpeexact", (BenchmarkLevel.INDEP, [500, 1000])), - # ("wstate", (BenchmarkLevel.INDEP, [500, 1000])), - # ("qaoa", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), - # ("vqe_two_local", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), + ("graphstate", (BenchmarkLevel.INDEP, [60, 80, 100, 120, 140, 160, 180, 200, 500, 1000, 2000, 5000])), + ("qft", (BenchmarkLevel.INDEP, [500, 1000])), + ("qpeexact", (BenchmarkLevel.INDEP, [500, 1000])), + ("wstate", (BenchmarkLevel.INDEP, [500, 1000])), + ("qaoa", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), + ("vqe_two_local", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), ]): qc.qasm3(f"in/{benchmark}_n{qc.num_qubits}.qasm") process_benchmark(astar_compiler, "astar", qc, benchmark, evaluator) process_benchmark(ids_compiler, "ids", qc, benchmark, evaluator) process_benchmark(relaxed_compiler, "relaxed", qc, benchmark, evaluator) + print( + "\033[32m[INFO]\033[0m ==================================================================================, \n" + "\033[32m[INFO]\033[0m Now, \n" + "\033[32m[INFO]\033[0m - the results are located in `results.csv`,\n" + "\033[32m[INFO]\033[0m - the input circuits in the QASM format are located in\n" + "\033[32m[INFO]\033[0m the `in` directory,\n" + "\033[32m[INFO]\033[0m - the compiled circuits in the naviz format are located\n" + "\033[32m[INFO]\033[0m in the `out` directory separated for each compiler and\n" + "\033[32m[INFO]\033[0m setting, and\n" + "\033[32m[INFO]\033[0m - the architecture specification compatible with NAViz is\n" + "\033[32m[INFO]\033[0m located in `arch.namachine`\n" + "\033[32m[INFO]\033[0m \n" + "\033[32m[INFO]\033[0m The generated `.naviz` files can be animated with the\n" + "\033[32m[INFO]\033[0m MQT NAViz tool." + ) + TIMEOUT = 15 * 60 From 51b2acca470f819a5c30dcbb318a0f5cdccc9815 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:25:32 +0100 Subject: [PATCH 094/119] =?UTF-8?q?=E2=9E=95=20Add=20naviz=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .license-tools-config.json | 3 +- eval/na/zoned/mqt.nastyle | 193 +++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 eval/na/zoned/mqt.nastyle diff --git a/.license-tools-config.json b/.license-tools-config.json index 6236193c3..4dee573ae 100644 --- a/.license-tools-config.json +++ b/.license-tools-config.json @@ -14,7 +14,8 @@ ".td": "SLASH_STYLE", ".yaml": "POUND_STYLE", ".toml": "POUND_STYLE", - ".yml": "POUND_STYLE" + ".yml": "POUND_STYLE", + ".nastyle": "SLASH_STYLE" }, "exclude": [ "^\\.[^/]+", diff --git a/eval/na/zoned/mqt.nastyle b/eval/na/zoned/mqt.nastyle new file mode 100644 index 000000000..c8ec5a79d --- /dev/null +++ b/eval/na/zoned/mqt.nastyle @@ -0,0 +1,193 @@ +// Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +// Copyright (c) 2025 Munich Quantum Software Company GmbH +// All rights reserved. +// +// SPDX-License-Identifier: MIT +// +// Licensed under the MIT License + +name: "MQT debug" + +atom { + trapped { + color: #0065BD + } + shuttling { + color: #A2AD00 + } + legend { + name { + ^atom[_-]?(.*)$: "" + ^.*$: "" + } + font { + family: "CMU Sans Serif" + size: 2 + color: #000000 + } + } + radius: 1 + margin: 1 +} + +zone { + config ^(zone[_-]?)?cz.*$ { + color: #e37222 + line { + thickness: 0.5 + dash { + length: 0 + duty: 100% + } + } + name: "CZ" + } + config ^_(zone[_-]?)?(.*)$ { + color: #98c6ea + line { + thickness: 0.5 + dash { + length: 0 + duty: 100% + } + } + name: "" + } + config ^(zone[_-]?)?(.*)$ { + color: #98c6ea + line { + thickness: 0.5 + dash { + length: 0 + duty: 100% + } + } + name: "$2" + } + legend { + display: true + title: "Zones" + } +} + +operation { + config { + ry { + color: #98C6EA + name: "RY" + radius: 150% + } + rz { + color: #A2AD00 + name: "RZ" + radius: 150% + } + cz { + color: #e37222 + name: "CZ" + radius: 150% + } + } + legend { + display: true + title: "Gates" + } +} + +machine { + trap { + color: #7F98AB + radius: 1 + line_width: 0.2 + name: "SLM" + } + shuttle { + color: #003359 + line { + thickness: 0.2 + dash { + length: 5 + duty: 50% + } + } + name: "AOD" + } + legend { + display: true + title: "Atom States" + } +} + +coordinate { + tick { + x: 10 + y: 10 + color: #999999 + line { + thickness: 0.1 + dash { + length: 0 + duty: 100% + } + } + display: true + } + number { + x { + distance: 20 + position: bottom + } + y { + distance: 20 + position: left + } + display: true + font { + family: "CMU Sans Serif" + size: 2 + color: #000000 + } + } + axis { + x: "x" + y: "y" + display: true + font { + family: "CMU Sans Serif" + size: 2 + color: #000000 + } + } + margin: 5 +} + +sidebar { + font { + family: "CMU Sans Serif" + size: 32 + color: #000000 + } + margin: 8 + padding { + color: 16 + heading: 52 + entry: 45 + } + color_radius: 16 +} + +time { + display: true + prefix: "Time: " + precision: 1 + font { + family: "CMU Sans Serif" + size: 12 + color: #000000 + } +} + +viewport { + margin: 4 + color: #ffffff +} From 46fa1952a5d23f1930e44aa73f4d3b0cbd2bdd1d Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:56:42 +0100 Subject: [PATCH 095/119] =?UTF-8?q?=F0=9F=8E=A8=20Finalize=20evaluation=20?= =?UTF-8?q?script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/eval_ids_relaxed_routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index 559c4a6ba..db353c41f 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -671,7 +671,7 @@ def main() -> None: process_benchmark(relaxed_compiler, "relaxed", qc, benchmark, evaluator) print( - "\033[32m[INFO]\033[0m ==================================================================================, \n" + "\033[32m[INFO]\033[0m =============================================================\n" "\033[32m[INFO]\033[0m Now, \n" "\033[32m[INFO]\033[0m - the results are located in `results.csv`,\n" "\033[32m[INFO]\033[0m - the input circuits in the QASM format are located in\n" From 36bb784298127f2c735f29da6d138d2af299a38f Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:17:29 +0100 Subject: [PATCH 096/119] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20coderabbit's=20s?= =?UTF-8?q?uggestion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 3655e82cf..d7bebf5e1 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -67,19 +67,18 @@ class PlacementMethod(Enum): """ class RoutingMethod(Enum): - """Enumeration of the available routing methods for the independent set router.""" + """Enumeration of the available routing methods for the independent set router. - strict = ... - """ - Strict routing, i.e., the relative order of atoms must be - maintained throughout a movement. - """ - relaxed = ... - """ - Relaxed routing, i.e., the relative order of atoms may change - throughout a movement by applying offsets during pick-up and drop-off. + Members: + strict: Strict routing, i.e., the relative order of atoms must be maintained + throughout a movement. + relaxed: Relaxed routing, i.e., the relative order of atoms may change + throughout a movement by applying offsets during pick-up and drop-off. """ + strict: RoutingMethod + relaxed: RoutingMethod + class RoutingAgnosticCompiler: """MQT QMAP's routing-agnostic Zoned Neutral Atom Compiler.""" From a673ab7133a280b3181801720aabad5f73eec53f Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:31:33 +0100 Subject: [PATCH 097/119] =?UTF-8?q?=F0=9F=93=9D=20Improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 11 +++++++---- .../router/IndependentSetRouter.cpp | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index fed2dab2b..1161c9fb7 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -194,7 +194,7 @@ class IndependentSetRouter : public RouterBase { const Placement& targetPlacement) const -> std::unordered_map>; /** - * Creates the relaxed and strict conflict graph. + * Creates both the strict and relaxed conflict graphs. * @details Atom/qubit indices are the nodes. Two nodes are connected if their * corresponding move with respect to the given @p start- and @p * targetPlacement stands in conflict with each other based on the strict @@ -210,9 +210,12 @@ class IndependentSetRouter : public RouterBase { * @param startPlacement is the start placement of all atoms as a mapping from * atoms to their sites. * @param targetPlacement is the target placement of the atoms. - * @return the relaxed conflict graph as an unordered_map where the keys are - * the nodes and the values are vectors of (neighbor, optional merge cost) - * pairs. + * @return a pair consisting of: + * - the strict conflict graph as an unordered_map where the keys are the + * nodes and the values are vectors of their neighbors, and + * - the relaxed conflict graph as an unordered_map where the keys are the + * nodes and the values are vectors of (neighbor, optional merge cost) + * pairs. */ [[nodiscard]] auto createRelaxedAndStrictConflictGraph(const std::vector& atomsToMove, diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index 55ac7cee0..d1a3adeb4 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -117,12 +117,14 @@ auto IndependentSetRouter::isCompatibleMovement( return true; } namespace { +/// @returns `(a^(1/3) + b^(1/3))^3` auto sumCubeRootsCubed(const double a, const double b) -> double { double x = std::cbrt(a); double y = std::cbrt(b); // (x+y)^3 = a + b + 3*x*y*(x+y) return a + b + 3.0 * x * y * (x + y); } +/// @returns `(a^(1/3) - b^(1/3))^3` auto subCubeRootsCubed(const double a, const double b) -> double { double x = std::cbrt(a); double y = std::cbrt(b); From ac9130293c6d657b172766083a6412d66e7a9602 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:17:29 +0100 Subject: [PATCH 098/119] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20coderabbit's=20s?= =?UTF-8?q?uggestion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 977fe0996..ae306cfdf 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -55,19 +55,18 @@ class ZonedNeutralAtomArchitecture: """ class RoutingMethod(Enum): - """Enumeration of the available routing methods for the independent set router.""" + """Enumeration of the available routing methods for the independent set router. - strict = ... - """ - Strict routing, i.e., the relative order of atoms must be - maintained throughout a movement. - """ - relaxed = ... - """ - Relaxed routing, i.e., the relative order of atoms may change - throughout a movement by applying offsets during pick-up and drop-off. + Members: + strict: Strict routing, i.e., the relative order of atoms must be maintained + throughout a movement. + relaxed: Relaxed routing, i.e., the relative order of atoms may change + throughout a movement by applying offsets during pick-up and drop-off. """ + strict: RoutingMethod + relaxed: RoutingMethod + class RoutingAgnosticCompiler: """MQT QMAP's routing-agnostic Zoned Neutral Atom Compiler.""" From 0b02d53ef366d73c759a0169ac48331799a118a2 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:31:33 +0100 Subject: [PATCH 099/119] =?UTF-8?q?=F0=9F=93=9D=20Improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 11 +++++++---- .../router/IndependentSetRouter.cpp | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index fed2dab2b..1161c9fb7 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -194,7 +194,7 @@ class IndependentSetRouter : public RouterBase { const Placement& targetPlacement) const -> std::unordered_map>; /** - * Creates the relaxed and strict conflict graph. + * Creates both the strict and relaxed conflict graphs. * @details Atom/qubit indices are the nodes. Two nodes are connected if their * corresponding move with respect to the given @p start- and @p * targetPlacement stands in conflict with each other based on the strict @@ -210,9 +210,12 @@ class IndependentSetRouter : public RouterBase { * @param startPlacement is the start placement of all atoms as a mapping from * atoms to their sites. * @param targetPlacement is the target placement of the atoms. - * @return the relaxed conflict graph as an unordered_map where the keys are - * the nodes and the values are vectors of (neighbor, optional merge cost) - * pairs. + * @return a pair consisting of: + * - the strict conflict graph as an unordered_map where the keys are the + * nodes and the values are vectors of their neighbors, and + * - the relaxed conflict graph as an unordered_map where the keys are the + * nodes and the values are vectors of (neighbor, optional merge cost) + * pairs. */ [[nodiscard]] auto createRelaxedAndStrictConflictGraph(const std::vector& atomsToMove, diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index 55ac7cee0..d1a3adeb4 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -117,12 +117,14 @@ auto IndependentSetRouter::isCompatibleMovement( return true; } namespace { +/// @returns `(a^(1/3) + b^(1/3))^3` auto sumCubeRootsCubed(const double a, const double b) -> double { double x = std::cbrt(a); double y = std::cbrt(b); // (x+y)^3 = a + b + 3*x*y*(x+y) return a + b + 3.0 * x * y * (x + y); } +/// @returns `(a^(1/3) - b^(1/3))^3` auto subCubeRootsCubed(const double a, const double b) -> double { double x = std::cbrt(a); double y = std::cbrt(b); From 747195e05b4e9d0699b0a59243c916f2009b1540 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:47:21 +0100 Subject: [PATCH 100/119] =?UTF-8?q?Revert=20"=F0=9F=8E=A8=20Apply=20codera?= =?UTF-8?q?bbit's=20suggestion"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ac9130293c6d657b172766083a6412d66e7a9602. --- python/mqt/qmap/na/zoned.pyi | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index ae306cfdf..977fe0996 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -55,17 +55,18 @@ class ZonedNeutralAtomArchitecture: """ class RoutingMethod(Enum): - """Enumeration of the available routing methods for the independent set router. + """Enumeration of the available routing methods for the independent set router.""" - Members: - strict: Strict routing, i.e., the relative order of atoms must be maintained - throughout a movement. - relaxed: Relaxed routing, i.e., the relative order of atoms may change - throughout a movement by applying offsets during pick-up and drop-off. + strict = ... + """ + Strict routing, i.e., the relative order of atoms must be + maintained throughout a movement. + """ + relaxed = ... + """ + Relaxed routing, i.e., the relative order of atoms may change + throughout a movement by applying offsets during pick-up and drop-off. """ - - strict: RoutingMethod - relaxed: RoutingMethod class RoutingAgnosticCompiler: """MQT QMAP's routing-agnostic Zoned Neutral Atom Compiler.""" From 3d9a446a00522c8a1d64e6d3668a3772493fd241 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:17:29 +0100 Subject: [PATCH 101/119] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20coderabbit's=20s?= =?UTF-8?q?uggestion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index ed06350b2..218a5d228 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -67,19 +67,18 @@ class PlacementMethod(Enum): """ class RoutingMethod(Enum): - """Enumeration of the available routing methods for the independent set router.""" + """Enumeration of the available routing methods for the independent set router. - strict = ... - """ - Strict routing, i.e., the relative order of atoms must be - maintained throughout a movement. - """ - relaxed = ... - """ - Relaxed routing, i.e., the relative order of atoms may change - throughout a movement by applying offsets during pick-up and drop-off. + Members: + strict: Strict routing, i.e., the relative order of atoms must be maintained + throughout a movement. + relaxed: Relaxed routing, i.e., the relative order of atoms may change + throughout a movement by applying offsets during pick-up and drop-off. """ + strict: RoutingMethod + relaxed: RoutingMethod + class RoutingAgnosticCompiler: """MQT QMAP's routing-agnostic Zoned Neutral Atom Compiler.""" From 18dfdcfc36d3ded4daef375313aca233a15ffdc3 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:31:33 +0100 Subject: [PATCH 102/119] =?UTF-8?q?=F0=9F=93=9D=20Improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/IndependentSetRouter.hpp | 11 +++++++---- .../router/IndependentSetRouter.cpp | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp index fed2dab2b..1161c9fb7 100644 --- a/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp +++ b/include/na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp @@ -194,7 +194,7 @@ class IndependentSetRouter : public RouterBase { const Placement& targetPlacement) const -> std::unordered_map>; /** - * Creates the relaxed and strict conflict graph. + * Creates both the strict and relaxed conflict graphs. * @details Atom/qubit indices are the nodes. Two nodes are connected if their * corresponding move with respect to the given @p start- and @p * targetPlacement stands in conflict with each other based on the strict @@ -210,9 +210,12 @@ class IndependentSetRouter : public RouterBase { * @param startPlacement is the start placement of all atoms as a mapping from * atoms to their sites. * @param targetPlacement is the target placement of the atoms. - * @return the relaxed conflict graph as an unordered_map where the keys are - * the nodes and the values are vectors of (neighbor, optional merge cost) - * pairs. + * @return a pair consisting of: + * - the strict conflict graph as an unordered_map where the keys are the + * nodes and the values are vectors of their neighbors, and + * - the relaxed conflict graph as an unordered_map where the keys are the + * nodes and the values are vectors of (neighbor, optional merge cost) + * pairs. */ [[nodiscard]] auto createRelaxedAndStrictConflictGraph(const std::vector& atomsToMove, diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index 55ac7cee0..d1a3adeb4 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -117,12 +117,14 @@ auto IndependentSetRouter::isCompatibleMovement( return true; } namespace { +/// @returns `(a^(1/3) + b^(1/3))^3` auto sumCubeRootsCubed(const double a, const double b) -> double { double x = std::cbrt(a); double y = std::cbrt(b); // (x+y)^3 = a + b + 3*x*y*(x+y) return a + b + 3.0 * x * y * (x + y); } +/// @returns `(a^(1/3) - b^(1/3))^3` auto subCubeRootsCubed(const double a, const double b) -> double { double x = std::cbrt(a); double y = std::cbrt(b); From 7452910911a7316fbf20e32e2c0054cde3088caf Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:43:14 +0100 Subject: [PATCH 103/119] =?UTF-8?q?=F0=9F=91=B7=20Update=20C++=20test=20ma?= =?UTF-8?q?trices=20(#874)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # CHANGELOG.md --- .github/workflows/ci.yml | 26 +++++--------------------- CHANGELOG.md | 9 +++++++++ CMakeLists.txt | 12 ++++++++++++ UPGRADING.md | 7 +++++++ pyproject.toml | 6 +++--- 5 files changed, 36 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9425e14c..bb7c07a2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,16 +88,9 @@ jobs: strategy: fail-fast: false matrix: - runs-on: - [ubuntu-22.04, ubuntu-24.04, ubuntu-22.04-arm, ubuntu-24.04-arm] - compiler: [gcc, clang, clang-19, clang-20] + runs-on: [ubuntu-24.04, ubuntu-24.04-arm] + compiler: [gcc, clang, clang-20, clang-21] config: [Release, Debug] - exclude: - # both combinations do not support C++20 due to the compiler being too old - - runs-on: ubuntu-22.04 - compiler: clang - - runs-on: ubuntu-22.04-arm - compiler: clang uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@89734354f64f30a80dd16602d4e271df34348987 # v1.17.4 with: runs-on: ${{ matrix.runs-on }} @@ -113,15 +106,9 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [macos-14, macos-15-intel, macos-15] - compiler: [clang, clang-19, clang-20, gcc-14, gcc-15] + runs-on: [macos-14, macos-15, macos-15-intel] + compiler: [clang, clang-20, clang-21, gcc-14, gcc-15] config: [Release, Debug] - exclude: - # see https://github.com/munich-quantum-toolkit/core/issues/979 - - runs-on: macos-15 - compiler: gcc-14 - - runs-on: macos-15 - compiler: gcc-15 uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@89734354f64f30a80dd16602d4e271df34348987 # v1.17.4 with: runs-on: ${{ matrix.runs-on }} @@ -211,10 +198,7 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [ubuntu-22.04, ubuntu-22.04-arm, macos-15, windows-2025] - exclude: - # see https://github.com/munich-quantum-toolkit/qmap/issues/703 - - runs-on: macos-15 + runs-on: [macos-15, windows-2025] uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@89734354f64f30a80dd16602d4e271df34348987 # v1.17.4 with: runs-on: ${{ matrix.runs-on }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd5dabf4..a2fb628de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,11 +17,19 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ ### Changed +- 👷 Stop testing on `ubuntu-22.04` and `ubuntu-22.04-arm` runners ([#874]) ([**@denialhaag**]) +- 👷 Stop testing with `clang-19` and start testing with `clang-21` ([#874]) ([**@denialhaag**]) +- 👷 Fix macOS tests with Homebrew Clang via new `munich-quantum-toolkit/workflows` version ([#874]) ([**@denialhaag**]) +- 👷 Re-enable macOS tests with GCC by disabling module scanning ([#874]) ([**@denialhaag**]) - ✨ Enable code generation for relaxed routing constraints ([#848]) ([**@ystade**]) - ✨ Add `max_filling_factor` to scheduler in Zoned Neutral Atom Compiler ([#847]) ([**@ystade**]) - ✨ Added extension to the hybrid routing mapper to also support Bridge gates, Passby moves and Flying ancillas ([#832]) ([**@lsschmid**]) - ✨ Added hybrid synthesis routing for iterative circuit constructions ([#832]) ([**@lsschmid**]) +### Removed + +- 🔥 Remove wheel builds for Python 3.13t ([#874]) ([**@denialhaag**]) + ## [3.4.0] - 2025-10-15 _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#340)._ @@ -164,6 +172,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ +[#874]: https://github.com/munich-quantum-toolkit/qmap/pull/874 [#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/CMakeLists.txt b/CMakeLists.txt index d5696d809..084331443 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,18 @@ endif() option(BUILD_MQT_QMAP_TESTS "Also build tests for the MQT QMAP project" ${MQT_QMAP_MASTER_PROJECT}) +# on macOS with GCC, disable module scanning +# https://www.reddit.com/r/cpp_questions/comments/1kwlkom/comment/ni5angh/ +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28") + set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + else() + message(WARNING "CMake 3.28+ is required to disable C++ module scanning on macOS with GCC. " + "Current version: ${CMAKE_VERSION}. " + "Consider upgrading CMake to avoid potential build issues.") + endif() +endif() + include(cmake/ExternalDependencies.cmake) # set the include directory for the build tree diff --git a/UPGRADING.md b/UPGRADING.md index 1daff2c89..64df652ee 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -20,6 +20,13 @@ Enabling/increasing the corresponding parameters allows enabling individually si The hybrid mapper now also optionally yields a `.naviz` output which can be handled similarly to the zoned architecture compiler. +### Removal of Python 3.13t wheels + +Free-threading Python was introduced as an experimental feature in Python 3.13. +It became stable in Python 3.14. +To conserve space on PyPI and to reduce the CD build times, we have removed all wheels for Python 3.13t from our CI. +We continue to provide wheels for the regular Python versions 3.10 to 3.14, as well as 3.14t. + ## [3.4.0] ### End of support for Python 3.9 diff --git a/pyproject.toml b/pyproject.toml index 46788f507..5d135700f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -316,7 +316,7 @@ ignore = ["GH200"] [tool.cibuildwheel] build = "cp3*" -skip = "*-musllinux*" +skip = ["*-musllinux_*", "cp313t-*"] archs = "auto64" test-groups = ["test"] test-command = "pytest {project}/test/python" @@ -327,7 +327,7 @@ manylinux-aarch64-image = "manylinux_2_28" manylinux-ppc64le-image = "manylinux_2_28" manylinux-s390x-image = "manylinux_2_28" test-skip = [ - "cp3*t-*", # no freethreading qiskit wheels + "cp3??t-*", # no freethreading qiskit wheels "cp*-win_arm64", # no numpy, qiskit, ... wheels ] @@ -353,7 +353,7 @@ environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" } repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} --ignore-missing-dependencies" [tool.cibuildwheel.windows] -before-build = "uv pip install delvewheel>=1.9.0" +before-build = "uv pip install delvewheel>=1.11.2" repair-wheel-command = """delvewheel repair -w {dest_dir} {wheel} --namespace-pkg mqt \ --exclude mqt-core-ir.dll \ --exclude mqt-core-qasm.dll \ From b1d8928429f1b59f30a908d14a722f688e4d8d51 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:43:08 +0100 Subject: [PATCH 104/119] =?UTF-8?q?=F0=9F=93=9D=20Update=20Changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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 From ae01d18eff1ec02dd9300210389acc4a87105440 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:47:21 +0100 Subject: [PATCH 105/119] =?UTF-8?q?Revert=20"=F0=9F=8E=A8=20Apply=20codera?= =?UTF-8?q?bbit's=20suggestion"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ac9130293c6d657b172766083a6412d66e7a9602. --- python/mqt/qmap/na/zoned.pyi | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index 218a5d228..ed06350b2 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -67,17 +67,18 @@ class PlacementMethod(Enum): """ class RoutingMethod(Enum): - """Enumeration of the available routing methods for the independent set router. + """Enumeration of the available routing methods for the independent set router.""" - Members: - strict: Strict routing, i.e., the relative order of atoms must be maintained - throughout a movement. - relaxed: Relaxed routing, i.e., the relative order of atoms may change - throughout a movement by applying offsets during pick-up and drop-off. + strict = ... + """ + Strict routing, i.e., the relative order of atoms must be + maintained throughout a movement. + """ + relaxed = ... + """ + Relaxed routing, i.e., the relative order of atoms may change + throughout a movement by applying offsets during pick-up and drop-off. """ - - strict: RoutingMethod - relaxed: RoutingMethod class RoutingAgnosticCompiler: """MQT QMAP's routing-agnostic Zoned Neutral Atom Compiler.""" From 0db8c4e0878d4ebacb571b44916d77fc6f52beac Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:55:46 +0100 Subject: [PATCH 106/119] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20stray=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/na/zoned/zoned.cpp | 1 + python/mqt/qmap/na/zoned.pyi | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bindings/na/zoned/zoned.cpp b/bindings/na/zoned/zoned.cpp index 1c169b043..86ba2bdbc 100644 --- a/bindings/na/zoned/zoned.cpp +++ b/bindings/na/zoned/zoned.cpp @@ -167,6 +167,7 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { na::zoned::RoutingAwareCompiler::Config config; config.logLevel = spdlog::level::from_str(logLevel); config.schedulerConfig.maxFillingFactor = maxFillingFactor; + config.layoutSynthesizerConfig.placerConfig = { .useWindow = useWindow, .windowMinWidth = windowMinWidth, diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index d7bebf5e1..ed06350b2 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -67,17 +67,18 @@ class PlacementMethod(Enum): """ class RoutingMethod(Enum): - """Enumeration of the available routing methods for the independent set router. + """Enumeration of the available routing methods for the independent set router.""" - Members: - strict: Strict routing, i.e., the relative order of atoms must be maintained - throughout a movement. - relaxed: Relaxed routing, i.e., the relative order of atoms may change - throughout a movement by applying offsets during pick-up and drop-off. + strict = ... + """ + Strict routing, i.e., the relative order of atoms must be + maintained throughout a movement. + """ + relaxed = ... + """ + Relaxed routing, i.e., the relative order of atoms may change + throughout a movement by applying offsets during pick-up and drop-off. """ - - strict: RoutingMethod - relaxed: RoutingMethod class RoutingAgnosticCompiler: """MQT QMAP's routing-agnostic Zoned Neutral Atom Compiler.""" @@ -160,8 +161,6 @@ class RoutingAwareCompiler: lookahead_factor: float = ..., reuse_level: float = ..., max_nodes: int = ..., - trials: int = ..., - queue_capacity: int = ..., routing_method: RoutingMethod = ..., prefer_split: float = ..., warn_unsupported_gates: bool = ..., From f04b57268a1c467997081b0c0fb9ccaf16f6de65 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:56:45 +0100 Subject: [PATCH 107/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20pyi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python/mqt/qmap/na/zoned.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index ed06350b2..3655e82cf 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -161,6 +161,8 @@ class RoutingAwareCompiler: lookahead_factor: float = ..., reuse_level: float = ..., max_nodes: int = ..., + trials: int = ..., + queue_capacity: int = ..., routing_method: RoutingMethod = ..., prefer_split: float = ..., warn_unsupported_gates: bool = ..., From c9546bdc61fb14d30ae9709c605243de44a7a227 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:00:09 +0100 Subject: [PATCH 108/119] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20variable=20for?= =?UTF-8?q?=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout_synthesizer/router/IndependentSetRouter.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp index d1a3adeb4..3736d31a7 100644 --- a/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp +++ b/src/na/zoned/layout_synthesizer/router/IndependentSetRouter.cpp @@ -409,7 +409,7 @@ auto IndependentSetRouter::getAtomsToMove( const Placement& startPlacement, const Placement& targetPlacement) const -> std::vector { std::set, std::greater<>> - atomsToMoveOrderedAscByDist; + atomsToMoveOrderedDescByDist; assert(startPlacement.size() == targetPlacement.size()); for (qc::Qubit atom = 0; atom < startPlacement.size(); ++atom) { const auto& [startSLM, startRow, startColumn] = startPlacement[atom]; @@ -419,14 +419,14 @@ auto IndependentSetRouter::getAtomsToMove( startColumn != targetColumn) { const auto distance = architecture_.get().distance( startSLM, startRow, startColumn, targetSLM, targetRow, targetColumn); - atomsToMoveOrderedAscByDist.emplace(distance, atom); + atomsToMoveOrderedDescByDist.emplace(distance, atom); } } std::vector atomsToMove; - atomsToMove.reserve(atomsToMoveOrderedAscByDist.size()); + atomsToMove.reserve(atomsToMoveOrderedDescByDist.size()); // put the atoms into the vector such they are ordered decreasingly by their // movement distance - std::ranges::copy(atomsToMoveOrderedAscByDist | std::views::values, + std::ranges::copy(atomsToMoveOrderedDescByDist | std::views::values, std::back_inserter(atomsToMove)); return atomsToMove; } From 6019b975a43de07eb4d6f3b7c913d4713cc14816 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:48:32 +0100 Subject: [PATCH 109/119] =?UTF-8?q?=F0=9F=93=9D=20Improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 31370e574..881ffac89 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -480,6 +480,13 @@ class HeuristicPlacer : public PlacerBase { } 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_); From 17176b0d262ee834c98bf7b5ed2057972c7a6ffe Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:48:32 +0100 Subject: [PATCH 110/119] =?UTF-8?q?=F0=9F=93=9D=20Improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 31370e574..881ffac89 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -480,6 +480,13 @@ class HeuristicPlacer : public PlacerBase { } 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_); From a0f8ca910feffbd15cdca3b28b1b29f14f7789c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 09:24:34 +0100 Subject: [PATCH 111/119] =?UTF-8?q?=E2=AC=86=EF=B8=8F=F0=9F=91=A8=E2=80=8D?= =?UTF-8?q?=F0=9F=92=BB=20Update=20actions/download-artifact=20action=20to?= =?UTF-8?q?=20v7=20(#878)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 5708d4062..113542cfd 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -48,7 +48,7 @@ jobs: url: https://pypi.org/p/mqt.qmap needs: [build-sdist, build-wheel] steps: - - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: cibw-* path: dist From ab83069205713726d65da1757ba98fa16910061d Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:10:58 +0100 Subject: [PATCH 112/119] =?UTF-8?q?=E2=9C=A8=20[NA]=20Add=20IDS=20Placemen?= =?UTF-8?q?t=20Method=20(#862)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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: - [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 <100073938+ystade@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Haag <121057143+denialhaag@users.noreply.github.com> # Conflicts: # include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp --- .../placer/HeuristicPlacer.hpp | 85 +++++++++++-------- pyproject.toml | 2 +- uv.lock | 27 ++++-- 3 files changed, 71 insertions(+), 43 deletions(-) diff --git a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp index 881ffac89..f5a5c9422 100644 --- a/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp +++ b/include/na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp @@ -70,7 +70,7 @@ class HeuristicPlacer : 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 @@ -155,9 +155,7 @@ class HeuristicPlacer : 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 140 - * Byte. Hence, allowing 10,000,000 nodes results in memory consumption of - * about 2 GB plus the size of the rest of the data structures. + * is raised. */ size_t maxNodes = 10'000'000; /** @@ -378,6 +376,8 @@ class HeuristicPlacer : public PlacerBase { 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. @@ -387,7 +387,7 @@ class HeuristicPlacer : public PlacerBase { assert(i < minHeap_.size()); while (i > 0) { size_t parent = (i - 1) / 2; - if (PriorityCompare{}(minHeap_[i]->value, minHeap_[parent]->value)) { + if (compare_(minHeap_[i]->value, minHeap_[parent]->value)) { std::swap(minHeap_[i], minHeap_[parent]); minHeap_[i]->minHeapIndex = i; minHeap_[parent]->minHeapIndex = parent; @@ -406,7 +406,7 @@ class HeuristicPlacer : public PlacerBase { assert(i < maxHeap_.size()); while (i > 0) { size_t parent = (i - 1) / 2; - if (PriorityCompare{}(maxHeap_[parent]->value, maxHeap_[i]->value)) { + if (compare_(maxHeap_[parent]->value, maxHeap_[i]->value)) { std::swap(maxHeap_[i], maxHeap_[parent]); maxHeap_[i]->maxHeapIndex = i; maxHeap_[parent]->maxHeapIndex = parent; @@ -428,13 +428,11 @@ class HeuristicPlacer : public PlacerBase { size_t smallest = i; if (leftChild < minHeap_.size() && - PriorityCompare{}(minHeap_[leftChild]->value, - minHeap_[smallest]->value)) { + compare_(minHeap_[leftChild]->value, minHeap_[smallest]->value)) { smallest = leftChild; } if (rightChild < minHeap_.size() && - PriorityCompare{}(minHeap_[rightChild]->value, - minHeap_[smallest]->value)) { + compare_(minHeap_[rightChild]->value, minHeap_[smallest]->value)) { smallest = rightChild; } if (smallest != i) { @@ -459,13 +457,11 @@ class HeuristicPlacer : public PlacerBase { size_t largest = i; if (leftChild < maxHeap_.size() && - PriorityCompare{}(maxHeap_[largest]->value, - maxHeap_[leftChild]->value)) { + compare_(maxHeap_[largest]->value, maxHeap_[leftChild]->value)) { largest = leftChild; } if (rightChild < maxHeap_.size() && - PriorityCompare{}(maxHeap_[largest]->value, - maxHeap_[rightChild]->value)) { + compare_(maxHeap_[largest]->value, maxHeap_[rightChild]->value)) { largest = rightChild; } if (largest != i) { @@ -493,7 +489,7 @@ class HeuristicPlacer : public PlacerBase { maxHeap_.reserve(heapCapacity_); } /** - * @returns the top element of the stack. + * @returns the top element of the priority queue. * @note If @ref empty returns `true`, calling this function is * undefined behavior. */ @@ -502,7 +498,7 @@ class HeuristicPlacer : public PlacerBase { return minHeap_.front()->value; } /** - * @returns the top element of the stack. + * @returns the top element of the priority queue. * @note If @ref empty returns `true`, calling this function is * undefined behavior. */ @@ -535,15 +531,26 @@ class HeuristicPlacer : public PlacerBase { std::swap(maxHeap_[i], maxHeap_.back()); maxHeap_.pop_back(); maxHeap_[i]->maxHeapIndex = i; - heapifyMaxHeapDown(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 stack is empty. + /// @returns `true` if the queue is empty. [[nodiscard]] auto empty() const -> bool { return minHeap_.empty(); } - /// @brief Inserts an element at the top. + /// @brief Inserts an element into the priority queue. auto push(ValueType&& value) -> void { assert(minHeap_.size() == maxHeap_.size()); if (heapCapacity_ > 0) { @@ -556,7 +563,7 @@ class HeuristicPlacer : public PlacerBase { } else { assert(minHeap_.size() == heapCapacity_); // if capacity is reached, only insert the value if smaller than max - if (PriorityCompare{}(value, maxHeap_.front()->value)) { + 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)); @@ -593,10 +600,12 @@ class HeuristicPlacer : public PlacerBase { * 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. + * @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 restarts IDS performs. + * @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. @@ -611,6 +620,9 @@ class HeuristicPlacer : public PlacerBase { 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 @@ -625,6 +637,8 @@ class HeuristicPlacer : public PlacerBase { 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}; @@ -702,9 +716,6 @@ class HeuristicPlacer : public PlacerBase { * 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 * 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. @@ -714,11 +725,11 @@ class HeuristicPlacer : 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 held in the priority queue before - * the search is aborted. + * @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 */ @@ -731,6 +742,9 @@ class HeuristicPlacer : public PlacerBase { 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 //===--------------------------------------------------------------------===// @@ -758,7 +772,7 @@ class HeuristicPlacer : public PlacerBase { //===--------------------------------------------------------------------===// // Perform A* search //===--------------------------------------------------------------------===// - while (openSet.size() < maxNodes && !openSet.empty()) { + while (!openSet.empty()) { auto itm = openSet.top(); openSet.pop(); // if a goal is reached, that is the shortest path to a goal under the @@ -776,13 +790,12 @@ class HeuristicPlacer : public PlacerBase { 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."); + 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 " 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/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" From 2e72e1093df9f9a311b21dac52f2e7d602b5e54f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:59:20 +0000 Subject: [PATCH 113/119] =?UTF-8?q?=E2=AC=86=EF=B8=8F=F0=9F=AA=9D=20Update?= =?UTF-8?q?=20patch=20updates=20(#877)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit) | repository | patch | `v0.14.8` -> `v0.14.9` | | [astral-sh/uv-pre-commit](https://redirect.github.com/astral-sh/uv-pre-commit) | repository | patch | `0.9.16` -> `0.9.17` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://redirect.github.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit) ### [`v0.14.9`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.14.9) [Compare Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.14.8...v0.14.9) See:
astral-sh/uv-pre-commit (astral-sh/uv-pre-commit) ### [`v0.9.17`](https://redirect.github.com/astral-sh/uv-pre-commit/releases/tag/0.9.17) [Compare Source](https://redirect.github.com/astral-sh/uv-pre-commit/compare/0.9.16...0.9.17) See:
--- ### Configuration 📅 **Schedule**: Branch creation - "every weekend" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/munich-quantum-toolkit/qmap). Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bc89fc62..f8d01f5f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,13 +66,13 @@ repos: # Ensure uv lock file is up-to-date - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.9.16 + rev: 0.9.17 hooks: - id: uv-lock # Python linting using ruff - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.8 + rev: v0.14.9 hooks: - id: ruff-check - id: ruff-format From 61a77ab1d817e2ae5de935164731efd843fc39bd Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 15 Dec 2025 18:35:49 +0100 Subject: [PATCH 114/119] =?UTF-8?q?=F0=9F=8E=A8=20Suggestions=20from=20cod?= =?UTF-8?q?e=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/eval_ids_relaxed_routing.py | 60 +++++++++++------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index db353c41f..da39098ad 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -16,6 +16,7 @@ import pathlib import queue import re +from itertools import chain from math import sqrt from multiprocessing import get_context from typing import TYPE_CHECKING @@ -29,12 +30,16 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator, Mapping from multiprocessing import Queue - from typing import Any, ParamSpec + from typing import Any, ParamSpec, TypeVar from mqt.core.ir import QuantumComputation P = ParamSpec("P") - R = ParamSpec("R") + R = TypeVar("R") + + +"""Timeout for running the benchmark in seconds.""" +TIMEOUT = 15 * 60 # sec def _proc_target(q: Queue, func: Callable[P, R], args: P.args, kwargs: P.kwargs) -> None: @@ -178,6 +183,7 @@ def process_benchmark( return False except RuntimeError as e: print(f"\033[31m[ERROR]\033[0m Failed ({e})") + evaluator.print_error(benchmark_name, qc, compiler_name, setting_name) return False code = "\n".join(line for line in code.splitlines() if not line.startswith("@+ u")) @@ -228,23 +234,7 @@ def __init__(self, arch: Mapping[str, Any], filename: str) -> None: self.arch = arch self.filename = filename - self.circuit_name = "" - self.num_qubits = 0 - self.setting = "" - self.two_qubit_gates = 0 - - self.scheduling_time = 0 - self.reuse_analysis_time = 0 - self.placement_time = 0 - self.routing_time = 0 - self.code_generation_time = 0 - self.total_time = 0 - - self.rearrangement_duration = 0.0 - self.two_qubit_gate_layer = 0 - self.max_two_qubit_gates = None - - self.atom_locations = {} + self.reset() def reset(self) -> None: """Reset the Evaluator.""" @@ -529,7 +519,7 @@ def evaluate(self, name: str, qc: QuantumComputation, setting: str, code: str, s self.atom_locations[atom_name] = (int(float(x)), int(float(y))) else: # put line back on top of iterator - it = iter([line, *list(it)]) + it = chain([line], it) break for line in it: @@ -543,9 +533,11 @@ def evaluate(self, name: str, qc: QuantumComputation, setting: str, code: str, s self._process_cz() elif line.startswith("@+ u"): self._process_u(line, it) - else: - assert line.startswith("@+ rz"), f"Unrecognized operation: {line}" + elif line.startswith("@+ rz"): self._process_rz(line, it) + else: + msg = f"Unrecognized operation: {line}" + raise ValueError(msg) def print_header(self) -> None: """Print the header of the CSV file.""" @@ -566,29 +558,38 @@ def print_data(self) -> None: f"{self.max_two_qubit_gates},{self.rearrangement_duration}\n" ) - def print_timeout(self, circuit_name: str, qc: QuantumComputation, compiler: str, setting: str) -> None: + def print_timeout(self, circuit_name: str, qc: QuantumComputation, setting: str) -> None: """Print the data of the CSV file. Args: circuit_name: Name of the circuit. qc: The quantum circuit. - compiler: Name of the compiler. setting: Compiler setting name. """ with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: - csv.write(f"{circuit_name},{qc.num_qubits},{compiler},{setting},timeout,,,,,,,,,,,,,,,,,,,,,,,,,\n") + csv.write(f"{circuit_name},{qc.num_qubits},{setting},timeout,,,,,,,,,\n") - def print_memout(self, circuit_name: str, qc: QuantumComputation, compiler: str, setting: str) -> None: + def print_memout(self, circuit_name: str, qc: QuantumComputation, setting: str) -> None: """Print the data of the CSV file. Args: circuit_name: Name of the circuit. qc: The quantum circuit. - compiler: Name of the compiler. setting: Compiler setting name. """ with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: - csv.write(f"{circuit_name},{qc.num_qubits},{compiler},{setting},memout,,,,,,,,,,,,,,,,,,,,,,,,,\n") + csv.write(f"{circuit_name},{qc.num_qubits},{setting},memout,,,,,,,,,\n") + + def print_error(self, circuit_name: str, qc: QuantumComputation, setting: str) -> None: + """Print the data of the CSV file. + + Args: + circuit_name: Name of the circuit. + qc: The quantum circuit. + setting: Compiler setting name. + """ + with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: + csv.write(f"{circuit_name},{qc.num_qubits},{setting},error,,,,,,,,,\n") def main() -> None: @@ -656,6 +657,7 @@ def main() -> None: evaluator = Evaluator(arch_dict, "results.csv") evaluator.print_header() + pathlib.Path("in").mkdir(exist_ok=True) for benchmark, qc in benchmarks([ ("graphstate", (BenchmarkLevel.INDEP, [60, 80, 100, 120, 140, 160, 180, 200, 500, 1000, 2000, 5000])), @@ -687,8 +689,6 @@ def main() -> None: ) -TIMEOUT = 15 * 60 - if __name__ == "__main__": # set working directory to script location os.chdir(pathlib.Path(pathlib.Path(__file__).resolve()).parent) From 52f6b4231b21693c014c44ee831037569bedc197 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:33:42 +0100 Subject: [PATCH 115/119] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Add=20README,=20im?= =?UTF-8?q?prove=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/README.md | 24 ++++++ eval/na/zoned/eval_ids_relaxed_routing.py | 100 +++++++++++----------- 2 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 eval/na/zoned/README.md diff --git a/eval/na/zoned/README.md b/eval/na/zoned/README.md new file mode 100644 index 000000000..131d32061 --- /dev/null +++ b/eval/na/zoned/README.md @@ -0,0 +1,24 @@ +# MQT QMAP's Zoned Neutral Atom Compiler Evaluation + +This document describes how to use the evaluation script for the zoned neutral atom compiler. +The script automates the process of running QMAP on a set of benchmark circuits and collecting performance metrics. + +## Prerequisites + +Before running the evaluation script, ensure you have the following installed: + +- uv 0.9.13 + +## Usage + +The main evaluation script is `evaluate_ids_relaxed_routing.py`. You can directly run it from the command line. + +```bash +evaluate_ids_relaxed_routing.py +``` + +## Output + +The script produces a CSV file named `results.csv` in the directory of the script. +Additionally, all input circuits are written to the `in` directory as QASM files. +Furthermore, the compiled files are written to the `out` directory ordered by compiler and settings. diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index da39098ad..085b03d99 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env -S uv run --script --quiet # Copyright (c) 2023 - 2025 Chair for Design Automation, TUM # Copyright (c) 2025 Munich Quantum Software Company GmbH # All rights reserved. @@ -6,6 +6,15 @@ # SPDX-License-Identifier: MIT # # Licensed under the MIT License +# +# /// script +# dependencies = [ +# "mqt.bench==2.1.0", +# "mqt.qmap @ git+https://github.com/munich-quantum-toolkit/qmap@28bf309dc166d87eed234bfa5dc2192e8900f865", +# ] +# [tool.uv] +# exclude-newer = "2025-12-16T00:00:00Z" +# /// """Script for evaluating the fast relaxed compiler.""" @@ -168,6 +177,9 @@ def process_benchmark( qc: The benchmark circuit to compile. benchmark_name: Name of the benchmark. evaluator: The evaluator to use. + + Returns: + True if compilation succeeded, False otherwise. """ compiler_name = type(compiler).__name__ print(f"\033[32m[INFO]\033[0m Compiling {benchmark_name} with {qc.num_qubits} qubits with {compiler_name}...") @@ -175,15 +187,15 @@ def process_benchmark( code, stats = run_with_process_timeout(_compile_wrapper, TIMEOUT, compiler, qc) except TimeoutError as e: print(f"\033[31m[ERROR]\033[0m Failed ({e})") - evaluator.print_timeout(benchmark_name, qc, compiler_name, setting_name) + evaluator.print_timeout(benchmark_name, qc, setting_name) return False except MemoryError as e: print(f"\033[31m[ERROR]\033[0m Failed ({e})") - evaluator.print_memout(benchmark_name, qc, compiler_name, setting_name) + evaluator.print_memout(benchmark_name, qc, setting_name) return False except RuntimeError as e: print(f"\033[31m[ERROR]\033[0m Failed ({e})") - evaluator.print_error(benchmark_name, qc, compiler_name, setting_name) + evaluator.print_error(benchmark_name, qc, setting_name) return False code = "\n".join(line for line in code.splitlines() if not line.startswith("@+ u")) @@ -272,9 +284,9 @@ def _process_load(self, line: str, it: Iterator[str]) -> None: next_line_stripped = next_line.strip() if next_line_stripped == "]": break - assert next_line_stripped in self.atom_locations, ( - f"Atom {next_line_stripped} not found in atom locations" - ) + if next_line_stripped not in self.atom_locations: + msg = f"Atom {next_line_stripped} not found in atom locations" + raise ValueError(msg) atoms.append(next_line_stripped) else: # Single atom load @@ -432,10 +444,11 @@ def _apply_move(self, moves: list[tuple[str, tuple[int, int]]]) -> None: ) max_distance = max(max_distance, distance) - t_d_max = 200 - d_max = 110 - jerk = 32 * d_max / t_d_max**3 # 0.00044 - v_max = d_max / t_d_max * 2 # 1.1 + # Movement timing model parameters (units: um, us) + t_d_max = 200 # Time to traverse max distance (us) + d_max = 110 # Maximum distance for cubic profile (um) + jerk = 32 * d_max / t_d_max**3 # 0.00044, Jerk constant (um/us³) + v_max = d_max / t_d_max * 2 # = 1.1, Maximum velocity (um/us) if max_distance <= d_max: rearrangement_time = 2 * (4 * max_distance / jerk) ** (1 / 3) @@ -463,7 +476,7 @@ def _apply_cz(self, atoms: list[str]) -> None: """ self.two_qubit_gate_layer += 1 self.max_two_qubit_gates = ( - max(self.max_two_qubit_gates, len(atoms) / 2) if self.max_two_qubit_gates else len(atoms) / 2 + max(self.max_two_qubit_gates, len(atoms) // 2) if self.max_two_qubit_gates else len(atoms) // 2 ) def _apply_u(self, atoms: list[str]) -> None: @@ -594,6 +607,8 @@ def print_error(self, circuit_name: str, qc: QuantumComputation, setting: str) - def main() -> None: """Main function for evaluating the fast relaxed compiler.""" + # set working directory to script location + os.chdir(pathlib.Path(pathlib.Path(__file__).resolve()).parent) print("\033[32m[INFO]\033[0m Reading in architecture...") with pathlib.Path("square_architecture.json").open(encoding="utf-8") as f: arch_dict = json.load(f) @@ -617,56 +632,41 @@ def main() -> None: routing_method=RoutingMethod.strict, warn_unsupported_gates=False, ) - ids_compiler = RoutingAwareCompiler( - arch, - log_level="error", - max_filling_factor=0.9, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - placement_method=PlacementMethod.ids, - deepening_factor=0.01, - deepening_value=0.0, - lookahead_factor=0.4, - reuse_level=5.0, - trials=4, - queue_capacity=100, - routing_method=RoutingMethod.strict, - warn_unsupported_gates=False, - ) + common_ids_config = { + "log_level": "error", + "max_filling_factor": 0.9, + "use_window": True, + "window_min_width": 16, + "window_ratio": 1.0, + "window_share": 0.8, + "placement_method": PlacementMethod.ids, + "deepening_factor": 0.01, + "deepening_value": 0.0, + "lookahead_factor": 0.4, + "reuse_level": 5.0, + "trials": 4, + "queue_capacity": 100, + "warn_unsupported_gates": False, + } + ids_compiler = RoutingAwareCompiler(arch, **common_ids_config, routing_method=RoutingMethod.strict) relaxed_compiler = RoutingAwareCompiler( - arch, - log_level="error", - max_filling_factor=0.9, - use_window=True, - window_min_width=16, - window_ratio=1.0, - window_share=0.8, - placement_method=PlacementMethod.ids, - deepening_factor=0.01, - deepening_value=0.0, - lookahead_factor=0.4, - reuse_level=5.0, - trials=4, - queue_capacity=100, - routing_method=RoutingMethod.relaxed, - prefer_split=1.0, - warn_unsupported_gates=False, + arch, **common_ids_config, routing_method=RoutingMethod.relaxed, prefer_split=1.0 ) evaluator = Evaluator(arch_dict, "results.csv") evaluator.print_header() pathlib.Path("in").mkdir(exist_ok=True) - for benchmark, qc in benchmarks([ + benchmark_list = [ ("graphstate", (BenchmarkLevel.INDEP, [60, 80, 100, 120, 140, 160, 180, 200, 500, 1000, 2000, 5000])), ("qft", (BenchmarkLevel.INDEP, [500, 1000])), ("qpeexact", (BenchmarkLevel.INDEP, [500, 1000])), ("wstate", (BenchmarkLevel.INDEP, [500, 1000])), ("qaoa", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), ("vqe_two_local", (BenchmarkLevel.INDEP, [50, 100, 150, 200])), - ]): + ] + + for benchmark, qc in benchmarks(benchmark_list): qc.qasm3(f"in/{benchmark}_n{qc.num_qubits}.qasm") process_benchmark(astar_compiler, "astar", qc, benchmark, evaluator) process_benchmark(ids_compiler, "ids", qc, benchmark, evaluator) @@ -690,6 +690,4 @@ def main() -> None: if __name__ == "__main__": - # set working directory to script location - os.chdir(pathlib.Path(pathlib.Path(__file__).resolve()).parent) main() From 8c1651b5906af7f22bfde7c0c48eab961a3010aa Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:45:03 +0100 Subject: [PATCH 116/119] =?UTF-8?q?=F0=9F=93=9D=20Add=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/eval_ids_relaxed_routing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index 085b03d99..d164cfa75 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -82,6 +82,7 @@ def run_with_process_timeout(func: Callable[P, R], timeout: float, *args: P.args TimeoutError: If the function times out. Exception: If the function raises an exception. """ + # "fork" context avoids pickling bound methods but is Unix/macOS only. ctx = get_context("fork") # use fork so bound methods don't need to be pickled on macOS/Unix q = ctx.Queue() p = ctx.Process(target=_proc_target, args=(q, func, args, kwargs)) From bb44bf92908850b08e1c00f5826bedc51f682d2b Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:03:58 +0100 Subject: [PATCH 117/119] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/eval_ids_relaxed_routing.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index d164cfa75..012febd46 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -16,7 +16,12 @@ # exclude-newer = "2025-12-16T00:00:00Z" # /// -"""Script for evaluating the fast relaxed compiler.""" +"""Script for evaluating the routing-aware zoned neutral atom compiler. + +In particular, it compares the iterative diving search method against +the A* search method. Additionally, it evaluates the impact of relaxed +routing. +""" from __future__ import annotations From b05224326353d24d2ff86c0233df0e8635fc33b7 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:07:48 +0100 Subject: [PATCH 118/119] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20suggestions=20fr?= =?UTF-8?q?om=20code=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/na/zoned/README.md | 4 ++-- eval/na/zoned/eval_ids_relaxed_routing.py | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/eval/na/zoned/README.md b/eval/na/zoned/README.md index 131d32061..7a247824a 100644 --- a/eval/na/zoned/README.md +++ b/eval/na/zoned/README.md @@ -11,10 +11,10 @@ Before running the evaluation script, ensure you have the following installed: ## Usage -The main evaluation script is `evaluate_ids_relaxed_routing.py`. You can directly run it from the command line. +The main evaluation script is `eval_ids_relaxed_routing.py`. You can run it from the repository root via: ```bash -evaluate_ids_relaxed_routing.py + uv run eval/na/zoned/eval_ids_relaxed_routing.py ``` ## Output diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index 012febd46..a4fa8d195 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -270,7 +270,7 @@ def reset(self) -> None: self.rearrangement_duration = 0.0 self.two_qubit_gate_layer = 0 - self.max_two_qubit_gates = None + self.max_two_qubit_gates = 0 self.atom_locations = {} @@ -298,8 +298,11 @@ def _process_load(self, line: str, it: Iterator[str]) -> None: # Single atom load match = re.match(r"@\+ load (\w+)", line) if match: - assert match.group(1) in self.atom_locations, f"Atom {match.group(1)} not found in atom locations" - atoms.append(match.group(1)) + atom = match.group(1) + if atom not in self.atom_locations: + msg = f"Atom {atom} not found in atom locations" + raise ValueError(msg) + atoms.append(atom) self._apply_load(atoms) def _process_move(self, line: str, it: Iterator[str]) -> None: @@ -481,9 +484,7 @@ def _apply_cz(self, atoms: list[str]) -> None: atoms: List of atoms involved in the cz operation. """ self.two_qubit_gate_layer += 1 - self.max_two_qubit_gates = ( - max(self.max_two_qubit_gates, len(atoms) // 2) if self.max_two_qubit_gates else len(atoms) // 2 - ) + self.max_two_qubit_gates = max(self.max_two_qubit_gates, len(atoms) // 2) def _apply_u(self, atoms: list[str]) -> None: """Apply an u operation. From 7279666224018e4d09717966948e6f38bd38ca05 Mon Sep 17 00:00:00 2001 From: Yannick Stade <100073938+ystade@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:28:24 +0100 Subject: [PATCH 119/119] Apply suggestions from code review Co-authored-by: Lukas Burgholzer Signed-off-by: Yannick Stade <100073938+ystade@users.noreply.github.com> --- eval/na/zoned/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eval/na/zoned/README.md b/eval/na/zoned/README.md index 7a247824a..9be6747c2 100644 --- a/eval/na/zoned/README.md +++ b/eval/na/zoned/README.md @@ -7,14 +7,14 @@ The script automates the process of running QMAP on a set of benchmark circuits Before running the evaluation script, ensure you have the following installed: -- uv 0.9.13 +- uv 0.9.13+ ## Usage The main evaluation script is `eval_ids_relaxed_routing.py`. You can run it from the repository root via: ```bash - uv run eval/na/zoned/eval_ids_relaxed_routing.py + eval/na/zoned/eval_ids_relaxed_routing.py ``` ## Output