diff --git a/CHANGELOG.md b/CHANGELOG.md index c1aaa6392..d35321f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ ### Added +- ✨ Add Native Gate Decomposer to Zoned Neutral Atom Compiler ([#1050]) + ([**@ystade**]) - 🎨 Move auxiliary datastructures used in, e.g., NASP and NALAC from MQT Core to QMAP ([#1058]) ([**@ystade**]) @@ -270,6 +272,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ [#1058]: https://github.com/munich-quantum-toolkit/qmap/pull/1058 [#1057]: https://github.com/munich-quantum-toolkit/qmap/pull/1057 +[#1050]: https://github.com/munich-quantum-toolkit/qmap/pull/1050 [#1020]: https://github.com/munich-quantum-toolkit/qmap/pull/1020 [#953]: https://github.com/munich-quantum-toolkit/qmap/pull/953 [#924]: https://github.com/munich-quantum-toolkit/qmap/pull/924 diff --git a/UPGRADING.md b/UPGRADING.md index 130d45073..c516e78f6 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,15 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +This release updates the minimum required `mqt-core` version to `v3.6.2`. + +The Zoned Neutral Atom Compiler got a component to transform single-qubit gates +into the native gate set of the target architecture. Previously, single qubit +gates were only piped through the compiler to the output. The +`NativeGateDecomposer` can be used, by using the +`RoutingAwareNativeGateCompiler` as the compiler. For an example how to use it, +also see the script `eval/na/zined/eval_native_gate_decomposition.py`. + ## [3.7.0] This release also updates the minimum required `mqt-core` version to `v3.6.0` as diff --git a/bindings/na/register_zoned.cpp b/bindings/na/register_zoned.cpp index 272575cd2..9a6a0d183 100644 --- a/bindings/na/register_zoned.cpp +++ b/bindings/na/register_zoned.cpp @@ -179,7 +179,7 @@ void registerZoned(nb::module_& m) { // NOLINTNEXTLINE(misc-include-cleaner) return {arch, nlohmann::json::parse(json)}; }, - "arch"_a, "json"_a, + nb::keep_alive<0, 1>(), "arch"_a, "json"_a, R"pb(Create a compiler for the given architecture and with configurations from a JSON string. Args: @@ -214,7 +214,7 @@ void registerZoned(nb::module_& m) { const auto loads = json.attr("loads"); const nlohmann::json stats = self.getStatistics(); const auto dict = loads(stats.dump()); - return nb::cast>(dict); + return nb::cast>(dict); }, R"pb(Get the statistics of the last compilation as a JSON-style dictionary. @@ -332,7 +332,7 @@ void registerZoned(nb::module_& m) { // NOLINTNEXTLINE(misc-include-cleaner) return {arch, nlohmann::json::parse(json)}; }, - "arch"_a, "json"_a, + nb::keep_alive<0, 1>(), "arch"_a, "json"_a, R"pb(Create a compiler for the given architecture and configurations from a JSON string. Args: @@ -367,7 +367,170 @@ void registerZoned(nb::module_& m) { const auto loads = json.attr("loads"); const nlohmann::json stats = self.getStatistics(); const auto dict = loads(stats.dump()); - return nb::cast>(dict); + return nb::cast>(dict); + }, + R"pb(Get the statistics of the last compilation. + +Returns: + The statistics as a dictionary)pb"); + + //===--------------------------------------------------------------------===// + // Routing-aware Native Gate Compiler + //===--------------------------------------------------------------------===// + nb::class_ + routingAwareNativeGateCompiler( + m, "RoutingAwareNativeGateCompiler", + "Routing-aware native gate zoned neutral atom compiler."); + { + const na::zoned::RoutingAwareNativeGateCompiler::Config defaultConfig; + routingAwareNativeGateCompiler.def( + "__init__", + [](na::zoned::RoutingAwareNativeGateCompiler* self, + const na::zoned::Architecture& arch, const std::string& logLevel, + const double maxFillingFactor, const bool thetaOptSchedule, + const bool checkFinalCond, const bool useWindow, + const size_t windowMinWidth, const double windowRatio, + const double windowShare, + const na::zoned::HeuristicPlacer::Config::Method placementMethod, + const float deepeningFactor, const float deepeningValue, + const float lookaheadFactor, const float reuseLevel, + const size_t maxNodes, const size_t trials, + const size_t queueCapacity, + const na::zoned::IndependentSetRouter::Config::Method routingMethod, + const double preferSplit, const bool warnUnsupportedGates) { + na::zoned::RoutingAwareNativeGateCompiler::Config config; + config.logLevel = spdlog::level::from_str(logLevel); + config.schedulerConfig.maxFillingFactor = maxFillingFactor; + config.decomposerConfig = {.thetaOptSchedule = thetaOptSchedule, + .checkFinalCond = checkFinalCond}; + config.layoutSynthesizerConfig.placerConfig = { + .useWindow = useWindow, + .windowMinWidth = windowMinWidth, + .windowRatio = windowRatio, + .windowShare = windowShare, + .method = placementMethod, + .deepeningFactor = deepeningFactor, + .deepeningValue = deepeningValue, + .lookaheadFactor = lookaheadFactor, + .reuseLevel = reuseLevel, + .maxNodes = maxNodes, + .trials = trials, + .queueCapacity = queueCapacity, + }; + config.layoutSynthesizerConfig.routerConfig = { + .method = routingMethod, .preferSplit = preferSplit}; + config.codeGeneratorConfig = {.warnUnsupportedGates = + warnUnsupportedGates}; + new (self) na::zoned::RoutingAwareNativeGateCompiler{arch, config}; + }, + nb::keep_alive<1, 2>(), "arch"_a, + "log_level"_a = spdlog::level::to_short_c_str(defaultConfig.logLevel), + "max_filling_factor"_a = defaultConfig.schedulerConfig.maxFillingFactor, + "theta_opt_schedule"_a = + defaultConfig.decomposerConfig.thetaOptSchedule, + "check_final_cond"_a = defaultConfig.decomposerConfig.checkFinalCond, + "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, + "placement_method"_a = + defaultConfig.layoutSynthesizerConfig.placerConfig.method, + "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, + "trials"_a = defaultConfig.layoutSynthesizerConfig.placerConfig.trials, + "queue_capacity"_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, + R"pb(Create a routing-aware native gate compiler for the given architecture and configurations. + +Args: + arch: The zoned neutral atom architecture + log_level: The log level for the compiler, possible values are "debug"/"D", "info"/"I", "warning"/"W", "error"/"E", and "critical"/"C" + max_filling_factor: The maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel + theta_opt_schedule: If this setting is turned on, a re-scheduling pass is executed immediately after translating the gates into their U3 representation. The theta optimization tries to minimize the maximum theta per layer by possibly scheduling single-qubit gates in later layers. + check_final_cond: If enabled the theta optimization checks if the sum of the resulting layer's maximum theta and the next layer's maximum theta is strictly less than the sum of previous maximum thetas. This does not guarantee that the total schedule is the one with minimal cost but reduces the recursive calls by excluding some subsets. + use_window: Whether to use a window for the placer + window_min_width: The minimum width of the window for the placer + window_ratio: The ratio between the height and the width of the window + window_share: The share of free sites in the window in relation to the number of atoms to be moved in this step + placement_method: The placement method that should be used for the heuristic placer + deepening_factor: Controls the impact of the term in the heuristic of the A* search that resembles the standard deviation of the differences between the current and target sites of the atoms to be moved in every orientation + deepening_value: Is added to the sum of standard deviations before it is multiplied with the number of unplaced nodes and :attr:`deepening_factor` + lookahead_factor: Controls the lookahead's influence that considers the distance of atoms to their interaction partner in the next layer + reuse_level: The reuse level that corresponds to the estimated extra fidelity loss due to the extra trap transfers when the atom is not reused and instead moved to the storage zone and back to the entanglement zone + max_nodes: The maximum number of nodes that are considered in the A* search. + If this number is exceeded, the search is aborted and an error is raised. + In the current implementation, one node roughly consumes 120 Byte. + Hence, allowing 50,000,000 nodes results in memory consumption of about 6 GB plus the size of the rest of the data structures. + trials: The number of restarts during IDS. + queue_capacity: The maximum capacity of the priority queue used during IDS. + routing_method: The routing method that should be used for the independent set router + prefer_split: The threshold factor for group merging decisions during routing. + warn_unsupported_gates: Whether to warn about unsupported gates in the code generator)pb"); + } + + routingAwareNativeGateCompiler.def_static( + "from_json_string", + [](const na::zoned::Architecture& arch, + const std::string& json) -> na::zoned::RoutingAwareNativeGateCompiler { + // The correct header is included, but clang-tidy + // confuses it with the wrong forward header + // NOLINTNEXTLINE(misc-include-cleaner) + return {arch, nlohmann::json::parse(json)}; + }, + nb::keep_alive<0, 1>(), "arch"_a, "json"_a, + R"pb(Create a compiler for the given architecture and configurations from a JSON string. + +Args: + arch: The zoned neutral atom architecture + json: The JSON string + +Returns: + The initialized compiler + +Raises: + ValueError: If the string is not a valid JSON string)pb"); + + routingAwareNativeGateCompiler.def( + "compile", + [](na::zoned::RoutingAwareNativeGateCompiler& self, + const qc::QuantumComputation& qc) -> std::string { + return self.compile(qc).toString(); + }, + "qc"_a, + R"pb(Compile a quantum circuit for the zoned neutral atom architecture. + +Args: + qc: The quantum circuit + +Returns: + The compilation result as a string in the .naviz format.)pb"); + + routingAwareNativeGateCompiler.def( + "stats", + [](const na::zoned::RoutingAwareNativeGateCompiler& self) { + const auto json = nb::module_::import_("json"); + const auto loads = json.attr("loads"); + const nlohmann::json stats = self.getStatistics(); + const auto dict = loads(stats.dump()); + return nb::cast>(dict); }, R"pb(Get the statistics of the last compilation. diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index d41a4f566..f9485611c 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -41,11 +41,11 @@ if(BUILD_MQT_QMAP_BINDINGS) endif() # cmake-format: off -set(MQT_CORE_MINIMUM_VERSION 3.6.0 +set(MQT_CORE_MINIMUM_VERSION 3.6.2 CACHE STRING "MQT Core minimum version") -set(MQT_CORE_VERSION 3.6.1 +set(MQT_CORE_VERSION 3.6.2 CACHE STRING "MQT Core version") -set(MQT_CORE_REV "8224856776df527ff2e9911fb1751572fefaf80a" +set(MQT_CORE_REV "332818c3f035402d89d0f674dfdf3ccf9e622598" CACHE STRING "MQT Core identifier (tag, branch or commit hash)") set(MQT_CORE_REPO_OWNER "munich-quantum-toolkit" CACHE STRING "MQT Core repository owner (change when using a fork)") diff --git a/eval/na/zoned/eval_framework.py b/eval/na/zoned/eval_framework.py new file mode 100644 index 000000000..f782645a8 --- /dev/null +++ b/eval/na/zoned/eval_framework.py @@ -0,0 +1,662 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Framework for evaluating the zoned neutral atom compiler.""" + +from __future__ import annotations + +import pathlib +import queue +import re +import time +from itertools import chain +from math import 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 + +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 + + from mqt.qmap.na.zoned import RoutingAwareCompiler, RoutingAwareNativeGateCompiler + + P = ParamSpec("P") + 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: + """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: # noqa: BLE001 -- must forward any exception from the child process to the parent via the queue + 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. + """ + # "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)) + p.start() + deadline = time.monotonic() + timeout + try: + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + msg = f"Timed out after {timeout}s" + raise TimeoutError(msg) + try: + status, payload = q.get(timeout=min(remaining, 1.0)) + break + except queue.Empty: + if not p.is_alive(): + msg = f"Child process exited unexpectedly with code {p.exitcode}" + raise RuntimeError(msg) from None + 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: + circuit = get_benchmark(benchmark, mode, qubits) + transpiled = transpile_benchmark(benchmark, circuit) + qc = load(transpiled) + yield benchmark, qc + + +def _compile_wrapper( + compiler: RoutingAwareCompiler | RoutingAwareNativeGateCompiler, 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: RoutingAwareCompiler | RoutingAwareNativeGateCompiler, + setting_name: str, + qc: QuantumComputation, + benchmark_name: str, + evaluator: Evaluator, + *, + drop_u_gates: 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. + drop_u_gates: Whether to drop u gates in the output code. + + 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}...") + 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, setting_name) + return False + except MemoryError as e: + print(f"\033[31m[ERROR]\033[0m Failed ({e})") + 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, setting_name) + return False + + if drop_u_gates: + 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) + pathlib.Path(f"out/{compiler_name}/{setting_name}/{benchmark_name}_{qc.num_qubits}.naviz").write_text( + code, encoding="utf-8" + ) + 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, 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. + setting: Compiler setting name. + 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. + 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. + rearrangement_duration: Duration of rearrangement operations. + two_qubit_gate_layer: Number of two-qubit gate layers. + max_two_qubit_gates: Maximum number of two-qubit gates in a layer. + atom_locations: Dictionary of atom locations. + """ + + 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 = 0 + self.num_qubits = 0 + self.setting = 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.rearrangement_duration = 0.0 + self.two_qubit_gate_layer = 0 + self.max_two_qubit_gates = 0 + + self.atom_locations = {} + + def reset(self) -> None: + """Reset the Evaluator.""" + 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 = 0 + + self.atom_locations = {} + + 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 + 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 + match = re.match(r"@\+ load (\w+)", line) + if match: + 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) + else: + msg = f"Unrecognized load operation: {line}" + raise ValueError(msg) + 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() + if atom not in self.atom_locations: + msg = f"Atom {atom} not found in atom locations" + raise ValueError(msg) + 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() + if atom not in self.atom_locations: + msg = f"Atom {atom} not found in atom locations" + raise ValueError(msg) + moves.append((atom, (int(float(x)), int(float(y))))) + else: + msg = f"Unrecognized move operation: {line}" + raise ValueError(msg) + 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 + 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 store + match = re.match(r"@\+ store (\w+)", line) + if match: + if match.group(1) not in self.atom_locations: + msg = f"Atom {match.group(1)} not found in atom locations" + raise ValueError(msg) + atoms.append(match.group(1)) + else: + msg = f"Unrecognized store operation: {line}" + raise ValueError(msg) + 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) + if len(atoms) % 2 != 0: + msg = f"Expected even number of atoms in entanglement zone, got {len(atoms)}" + raise ValueError(msg) + 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 + 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 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)) + else: + msg = f"Unrecognized unitary (u) operation: {line}" + raise ValueError(msg) + 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 rz 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 + 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 rz + match = re.match(r"@\+ rz -?\d\.\d+ (\w+)", line) + if match: + if match.group(1) not in self.atom_locations: + msg = f"Atom {match.group(1)} not found in atom locations" + raise ValueError(msg) + atoms.append(match.group(1)) + else: + msg = f"Unrecognized rotation (rz) operation: {line}" + raise ValueError(msg) + self._apply_rz(atoms) + + def _process_ry(self, line: str, it: Iterator[str]) -> None: # noqa: ARG002 -- must have identical signature to the other functions to be exchangeable even though arguments and not used here + """Process a global ry operation. + + Args: + line: The current line being processed. + it: An iterator over the remaining lines. + """ + self._apply_global_ry() + + def _apply_load(self, _: list[str]) -> None: + """Apply a load operation. + + Args: + _: List of atoms to load. + """ + self.rearrangement_duration += 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. + """ + 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 + ) + max_distance = max(max_distance, distance) + + # 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) + else: + rearrangement_time = t_d_max + (max_distance - d_max) / v_max + self.rearrangement_duration += rearrangement_time + # Update atom locations + for atom, coord in moves: + if atom not in self.atom_locations: + msg = f"Atom {atom} not found in atom locations" + raise ValueError(msg) + self.atom_locations[atom] = coord + + def _apply_store(self, _: list[str]) -> None: + """Apply a store operation. + + Args: + _: List of atoms to store. + """ + self.rearrangement_duration += 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.two_qubit_gate_layer += 1 + 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. + + Args: + atoms: List of atoms involved in the u operation. + """ + + def _apply_global_u(self) -> None: + """Apply a global u operation.""" + + 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, qc: QuantumComputation, setting: str, code: str, stats: Mapping[str, Any]) -> None: + """Evaluate a circuit. + + Args: + name: Name of the circuit. + 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.setting = setting + 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 = chain([line], it) + break + + 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) + elif line.startswith("@+ rz"): + self._process_rz(line, it) + elif line.startswith("@+ ry"): + self._process_ry(line, it) + else: + msg = f"Unrecognized operation: {line}" + raise ValueError(msg) + + def print_header(self) -> None: + """Print the header of the CSV file.""" + pathlib.Path(self.filename).write_text( + "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", + encoding="utf-8", + ) + + def print_data(self) -> None: + """Print the data of the CSV file.""" + with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: + csv.write( + 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.max_two_qubit_gates},{self.rearrangement_duration}\n" + ) + + 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. + 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},timeout,,,,,,,,,,\n") + + 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. + 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},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") diff --git a/eval/na/zoned/eval_ids_relaxed_routing.py b/eval/na/zoned/eval_ids_relaxed_routing.py index ab8fca6bb..0775a1c90 100755 --- a/eval/na/zoned/eval_ids_relaxed_routing.py +++ b/eval/na/zoned/eval_ids_relaxed_routing.py @@ -11,6 +11,7 @@ # dependencies = [ # "mqt.bench==2.1.0", # "mqt.qmap==3.5.0", +# "qiskit==2.4.2", # ] # [tool.uv] # exclude-newer = "2025-12-16T12:59:59Z" @@ -28,592 +29,15 @@ import json import os import pathlib -import queue -import re -from itertools import chain -from math import 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 eval_framework import BenchmarkLevel, Evaluator, benchmarks, process_benchmark 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 mqt.core.ir import QuantumComputation - - P = ParamSpec("P") - 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: - """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: # noqa: BLE001 - 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. - """ - # "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)) - 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: - circuit = get_benchmark(benchmark, mode, qubits) - transpiled = transpile_benchmark(benchmark, circuit) - qc = load(transpiled) - yield benchmark, qc - - -def _compile_wrapper(compiler: RoutingAwareCompiler, 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: RoutingAwareCompiler, - setting_name: str, - qc: QuantumComputation, - benchmark_name: str, - evaluator: Evaluator, -) -> 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. - - 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}...") - 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, setting_name) - return False - except MemoryError as e: - print(f"\033[31m[ERROR]\033[0m Failed ({e})") - 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, setting_name) - 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) - pathlib.Path(f"out/{compiler_name}/{setting_name}/{benchmark_name}_{qc.num_qubits}.naviz").write_text( - code, encoding="utf-8" - ) - 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, 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. - setting: Compiler setting name. - 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. - 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. - rearrangement_duration: Duration of rearrangement operations. - two_qubit_gate_layer: Number of two-qubit gate layers. - max_two_qubit_gates: Maximum number of two-qubit gates in a layer. - atom_locations: Dictionary of atom locations. - """ - - 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.reset() - - def reset(self) -> None: - """Reset the Evaluator.""" - 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 = 0 - - self.atom_locations = {} - - 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 - 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 - match = re.match(r"@\+ load (\w+)", line) - if match: - 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: - """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, _: list[str]) -> None: - """Apply a load operation. - - Args: - _: List of atoms to load. - """ - self.rearrangement_duration += 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. - """ - 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 - ) - max_distance = max(max_distance, distance) - - # 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) - else: - rearrangement_time = t_d_max + (max_distance - d_max) / v_max - 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, _: list[str]) -> None: - """Apply a store operation. - - Args: - _: List of atoms to store. - """ - self.rearrangement_duration += 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.two_qubit_gate_layer += 1 - 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. - - Args: - atoms: List of atoms involved in the u operation. - """ - - def _apply_global_u(self) -> None: - """Apply a global u operation.""" - - 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, qc: QuantumComputation, setting: str, code: str, stats: Mapping[str, Any]) -> None: - """Evaluate a circuit. - - Args: - name: Name of the circuit. - 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.setting = setting - 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 = chain([line], it) - break - - 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) - 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.""" - pathlib.Path(self.filename).write_text( - "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", - encoding="utf-8", - ) - - def print_data(self) -> None: - """Print the data of the CSV file.""" - with pathlib.Path(self.filename).open("a", encoding="utf-8") as csv: - csv.write( - 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.max_two_qubit_gates},{self.rearrangement_duration}\n" - ) - - 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. - 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},timeout,,,,,,,,,\n") - - 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. - 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},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: """Main function for evaluating the fast relaxed compiler.""" - # set working directory to script location + # set the working directory to the 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: @@ -674,9 +98,9 @@ def main() -> None: 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) - process_benchmark(relaxed_compiler, "relaxed", qc, benchmark, evaluator) + process_benchmark(astar_compiler, "astar", qc, benchmark, evaluator, drop_u_gates=True) + process_benchmark(ids_compiler, "ids", qc, benchmark, evaluator, drop_u_gates=True) + process_benchmark(relaxed_compiler, "relaxed", qc, benchmark, evaluator, drop_u_gates=True) print( "\033[32m[INFO]\033[0m =============================================================\n" diff --git a/eval/na/zoned/eval_native_gate_decomposition.py b/eval/na/zoned/eval_native_gate_decomposition.py new file mode 100755 index 000000000..39c8e5276 --- /dev/null +++ b/eval/na/zoned/eval_native_gate_decomposition.py @@ -0,0 +1,113 @@ +#!/usr/bin/env -S uv run --script --quiet +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +# /// script +# dependencies = [ +# "mqt.bench==2.1.0", +# "mqt.qmap @ git+https://github.com/cda-tum/mqt-qmap@e6048e0fce5ed10468dde74aa1571a274da5d8df", +# "qiskit==2.4.2", +# ] +# [tool.uv] +# exclude-newer = "2026-07-06T23:59:59Z" +# /// + +"""Script for evaluating the routing-aware native gate zoned neutral atom compiler. + +In particular, it runs the native gate compiler to produce hardware compliant output. +It records central metrics of the compilation runs and the generated code. It compares +two different settings to evaluate the effectiveness of the theta optimization. +""" + +from __future__ import annotations + +import json +import os +import pathlib + +from eval_framework import BenchmarkLevel, Evaluator, benchmarks, process_benchmark + +from mqt.qmap.na.zoned import ( + PlacementMethod, + RoutingAwareCompiler, + RoutingAwareNativeGateCompiler, + RoutingMethod, + ZonedNeutralAtomArchitecture, +) + + +def main() -> None: + """Main function for evaluating the native gate compiler.""" + # set the working directory to the 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) + arch = ZonedNeutralAtomArchitecture.from_json_file("square_architecture.json") + arch.to_namachine_file("arch.namachine") + print("\033[32m[INFO]\033[0m Done") + common_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, + "routing_method": RoutingMethod.relaxed, + "prefer_split": 1.0, + "warn_unsupported_gates": False, + } + baseline = RoutingAwareCompiler(arch, **common_config) + setting1 = RoutingAwareNativeGateCompiler(arch, **common_config) + setting2 = RoutingAwareNativeGateCompiler(arch, **common_config, theta_opt_schedule=True) + + evaluator = Evaluator(arch_dict, "results.csv") + evaluator.print_header() + pathlib.Path("in").mkdir(exist_ok=True) + + benchmark_list = [ + ("graphstate", (BenchmarkLevel.INDEP, [20, 100])), + ("qft", (BenchmarkLevel.INDEP, [20, 100])), + ("qpeexact", (BenchmarkLevel.INDEP, [20, 100])), + ("wstate", (BenchmarkLevel.INDEP, [20, 100])), + ("qaoa", (BenchmarkLevel.INDEP, [20, 100])), + ("vqe_two_local", (BenchmarkLevel.INDEP, [20, 100])), + ] + + for benchmark, qc in benchmarks(benchmark_list): + qc.qasm3(f"in/{benchmark}_n{qc.num_qubits}.qasm") + process_benchmark(baseline, "baseline", qc, benchmark, evaluator) + process_benchmark(setting1, "setting1", qc, benchmark, evaluator) + process_benchmark(setting2, "setting2", 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." + ) + + +if __name__ == "__main__": + main() diff --git a/include/na/zoned/Compiler.hpp b/include/na/zoned/Compiler.hpp index 8ab7fdd59..381ec045c 100644 --- a/include/na/zoned/Compiler.hpp +++ b/include/na/zoned/Compiler.hpp @@ -10,18 +10,19 @@ #pragma once -#include "Architecture.hpp" -#include "code_generator/CodeGenerator.hpp" -#include "decomposer/NoOpDecomposer.hpp" #include "ir/QuantumComputation.hpp" #include "ir/operations/Operation.hpp" -#include "layout_synthesizer/PlaceAndRouteSynthesizer.hpp" -#include "layout_synthesizer/placer/HeuristicPlacer.hpp" -#include "layout_synthesizer/placer/VertexMatchingPlacer.hpp" -#include "layout_synthesizer/router/IndependentSetRouter.hpp" #include "na/NAComputation.hpp" -#include "reuse_analyzer/VertexMatchingReuseAnalyzer.hpp" -#include "scheduler/ASAPScheduler.hpp" +#include "na/zoned/Architecture.hpp" +#include "na/zoned/code_generator/CodeGenerator.hpp" +#include "na/zoned/decomposer/NativeGateDecomposer.hpp" +#include "na/zoned/decomposer/NoOpDecomposer.hpp" +#include "na/zoned/layout_synthesizer/PlaceAndRouteSynthesizer.hpp" +#include "na/zoned/layout_synthesizer/placer/HeuristicPlacer.hpp" +#include "na/zoned/layout_synthesizer/placer/VertexMatchingPlacer.hpp" +#include "na/zoned/layout_synthesizer/router/IndependentSetRouter.hpp" +#include "na/zoned/reuse_analyzer/VertexMatchingReuseAnalyzer.hpp" +#include "na/zoned/scheduler/ASAPScheduler.hpp" #include #include @@ -151,21 +152,19 @@ class Compiler : protected Scheduler, SPDLOG_DEBUG("Number of qubits: {}", qComp.getNqubits()); const auto nTwoQubitGates = static_cast( std::count_if(qComp.cbegin(), qComp.cend(), - [](const std::unique_ptr& op) { + [](const std::unique_ptr& op) -> bool { return op->getNqubits() == 2; })); SPDLOG_DEBUG("Number of two-qubit gates: {}", nTwoQubitGates); const auto nSingleQubitGates = static_cast( std::count_if(qComp.cbegin(), qComp.cend(), - [](const std::unique_ptr& op) { + [](const std::unique_ptr& op) -> bool { return op->getNqubits() == 1; })); SPDLOG_DEBUG("Number of single-qubit gates: {}", nSingleQubitGates); } #endif // SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG - // 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& [singleQubitGateLayers, twoQubitGateLayers] = @@ -203,7 +202,8 @@ class Compiler : protected Scheduler, const auto decomposingStart = std::chrono::system_clock::now(); const auto& [decomposedSingleQubitGateLayers, decomposedTwoQubitGateLayers] = - SELF.decompose(singleQubitGateLayers, twoQubitGateLayers); + SELF.decompose(qComp.getNqubits(), singleQubitGateLayers, + twoQubitGateLayers); const auto decomposingEnd = std::chrono::system_clock::now(); statistics_.decomposingTime = std::chrono::duration_cast(decomposingEnd - @@ -262,6 +262,9 @@ class Compiler : protected Scheduler, } }; +/** + * Concrete synthesizer that performs routing-agnostic layout synthesis. + */ class RoutingAgnosticSynthesizer : public PlaceAndRouteSynthesizer { @@ -300,6 +310,12 @@ class RoutingAwareSynthesizer : PlaceAndRouteSynthesizer(architecture) {} }; +/** + * Concrete compiler that schedules and performs routing-aware layout synthesis. + * + * In particular, it leaves the decomposition of gates into native gates to the + * user, i.e., it does not perform any decomposition of gates into native gates. + */ class RoutingAwareCompiler final : public Compiler { +public: + RoutingAwareNativeGateCompiler(const Architecture& architecture, + const Config& config) + : Compiler(architecture, config) {} + + explicit RoutingAwareNativeGateCompiler(const Architecture& architecture) + : Compiler(architecture) {} +}; } // namespace na::zoned diff --git a/include/na/zoned/decomposer/DecomposerBase.hpp b/include/na/zoned/decomposer/DecomposerBase.hpp index 218996dc6..e0b2633a8 100644 --- a/include/na/zoned/decomposer/DecomposerBase.hpp +++ b/include/na/zoned/decomposer/DecomposerBase.hpp @@ -27,6 +27,7 @@ class DecomposerBase { * * The decomposer may change the layering produced by the scheduler and, * hence, it receives the single-qubit and two-qubit gate layers. + * @param nQubits is the number of qubits in the scheduled circuit. * @param singleQubitGateLayers are the layers of single-qubit gates that are * meant to be first decomposed into the native gate set. * @param twoQubitGateLayers are the layers of two-qubit gates that the @@ -38,7 +39,8 @@ class DecomposerBase { * layer more than two-qubit gate layers. */ [[nodiscard]] virtual auto - decompose(const std::vector& singleQubitGateLayers, + decompose(size_t nQubits, + const std::vector& singleQubitGateLayers, const std::vector& twoQubitGateLayers) const -> DecompositionResult = 0; }; diff --git a/include/na/zoned/decomposer/NativeGateDecomposer.hpp b/include/na/zoned/decomposer/NativeGateDecomposer.hpp new file mode 100644 index 000000000..6cf50c1ae --- /dev/null +++ b/include/na/zoned/decomposer/NativeGateDecomposer.hpp @@ -0,0 +1,460 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "na/zoned/Types.hpp" +#include "na/zoned/decomposer/DecomposerBase.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace na::zoned { +/** + * Hasher for subproblems [twoQubitGates, singleQubitGates, remainingGates]. + */ +struct SubproblemHasher { + auto + operator()(const std::array, 3>& array) const noexcept + -> size_t { + size_t seed = 0; + std::ranges::for_each(array, [&seed](const std::vector& v) -> void { + qc::hashCombine(seed, std::hash{}(v.size())); + std::ranges::for_each(v, [&seed](const size_t node) -> void { + qc::hashCombine(seed, std::hash{}(node)); + }); + }); + return seed; + } +}; +/** + * Decomposes a given schedule of operations into the native gate set and, if + * `thetaOptScheduling` is enabled, re-schedules them to minimize the total + * global rotation angle theta across the circuit + */ +class NativeGateDecomposer : public DecomposerBase { +public: + /** + * A struct to store the decomposition angles of a U3 gate. + */ + struct Angles { + qc::fp theta = 0; + qc::fp phi = 0; + qc::fp lambda = 0; + }; + + /** + * A quaternion is represented by an array of four `qc::fp` values `{q0, q1, + * q2, q3}` denoting the components of the quaternion. The default initialized + * Quaternion denotes the identity, i.e., the neutral element, e.g., when + * calling @ref combineQuaternions with the identity quaternion, the other + * quaternion is returned. + */ + struct Quaternion { + qc::fp a = 1; + qc::fp b = 0; + qc::fp c = 0; + qc::fp d = 0; + }; + + /** + * A minimal struct to store the parameters of a U3 gate along with the qubit + * it acts on. + */ + struct U3Gate { + Angles angles; + qc::Qubit qubit = 0; + }; + + /// A value to use as a margin of error for float equality + constexpr static qc::fp epsilon = + std::numeric_limits::epsilon() * 1024; + + /// The configuration of the NativeGateDecomposer + struct Config { + bool thetaOptSchedule = false; + bool checkFinalCond = false; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, thetaOptSchedule, + checkFinalCond); + }; + +private: + /// The configuration of the NativeGateDecomposer + Config config_; + +public: + /// Create a new NativeGateDecomposer. + NativeGateDecomposer(const Architecture& /* unused */, const Config& config); + + /** + * @brief Converts commonly used single qubit gates into their Quaternion + * representation. + * @details A single qubit gate R_v(phi) with rotation axis v=(v0,v1,v2) + * and rotation angle phi can be represented as a quaternion: + * @code quaternion(R_v(phi)) = (cos(phi/2) * I, v0 * sin(phi/2) * X, v1 * + * sin(phi/2) * Y, v2 * sin(phi/2) * Z)@endcode with X, Y, Z Pauli Matrices. + * @param op a reference_wrapper to the operation to be converted + * @returns a quaternion. + */ + static auto + convertGateToQuaternion(std::reference_wrapper op) + -> Quaternion; + /** + * @brief Merges the quaternions representing two gates as in a matrix + * multiplication of the gates. + * @param q1 the first quaternion to be combined. + * @param q2 the second quaternion to be combined. + * @returns an quaternion. + */ + static auto combineQuaternions(const Quaternion& q1, const Quaternion& q2) + -> Quaternion; + /** + * @brief Calculates the values of the U3-gate parameters theta, phi, and + * lambda. + * @param quat is a quaternion representing a single qubit gate. + * @returns an array of three `qc::fp` values `{theta, phi, lambda}` giving + * the U3 gate angles. + */ + static auto getU3AnglesFromQuaternion(const Quaternion& quat) -> Angles; + + /** + * @brief Calculates the largest value of the U3-gate parameter theta from a + * vector of operations. + * @param layers is a vector of U3 parameters. + * @returns the maximal value of theta in the given layer. + */ + static auto calcThetaMax(const std::vector& layers) -> qc::fp; + + /** + * @brief Takes a vector of SingleQubitGateLayers and, for each layer, + * transforms all gates into U3 gates represented by `StructU3` objects. + * @details It combines all gates acting on the same qubit into a single U3 + * gate. + * @param layers is a std::vector of SingleQubitGateLayers of a scheduled + * circuit. + * @param nQubits is the number of qubits in the scheduled circuit. + * @returns a vector of vectors of StructU3 objects representing the single + * qubit gate layers. + */ + [[nodiscard]] static auto + transformToU3(const std::vector& layers, + size_t nQubits) -> std::vector>; + /** + * @brief Calculates the decomposition angles of a U3 gate + * @details Takes a vector of `qc::fp` representing the U3-gate angles of a + * single-qubit gate and the maximal value of theta for the single qubit gate + * layer and calculates the transversal decomposition angles as in Nottingham + * et. al. 2024. + * @param angles `std::array` of `qc::fp` representing (theta, phi, + * lambda). + * @param thetaMax the maximal theta value of the single-qubit gate layer. + * @returns an array of `qc::fp` values giving the angles (chi, gammaMinus, + * gammaPlus). + */ + auto static getDecompositionAngles(const Angles& angles, qc::fp thetaMax) + -> Angles; + + /** + * @brief Decomposes single-qubit and two-qubit gate layers into the native + * gate set (global y-rotations and local z-rotations), optionally applying + * theta-optimization scheduling. + * @param nQubits the number of qubits in the circuit. + * @param singleQubitGateLayers the single-qubit gate layers to decompose. + * @param twoQubitGateLayers the two-qubit gate layers of the schedule. + * @returns the decomposition result. + */ + [[nodiscard]] auto + decompose(size_t nQubits, + const std::vector& singleQubitGateLayers, + const std::vector& twoQubitGateLayers) const + -> DecompositionResult override; + + /** + * A class implementing a simple DiGraph for use in the scheduling + * component of the native gate decomposer. + * @tparam T is the type of object associated with each node + */ + template class DirectedGraph { + /// number of nodes in the graph + size_t nNodes_ = 0; + /// a vector containing the adjacency lists of each node + std::vector>> adjacencies_; + /// a vector containing the values associated with each node + std::vector nodeValues_; + + public: + /// Creates an empty graph to hold objects of type T. + DirectedGraph() {} + + /** + * @brief Adds a node with a given value to the graph. + * @param node the type T value to be added to the graph. + * @returns the node index of the created node. + */ + auto addNode(T&& node) -> size_t { + adjacencies_.emplace_back(); + nodeValues_.emplace_back(std::move(node)); + return nNodes_++; + } + + /** + * @brief Adds an edge between two nodes to the graph with the given weight. + * @param from is the index of the node from which the edge originates. + * @param to is the index of the node the edge is going to. + * @param weight is the weight of the edge, defaulting to 1.0. + * @returns a bool indicating if adding the edge was successful. + */ + auto addEdge(const size_t from, const size_t to, const double weight = 1.0) + -> bool { + if (from < nNodes_ && to < nNodes_ && from != to) { + adjacencies_[from].emplace_back(to, weight); + return true; + } + return false; + } + + /** + * @brief Gets the value of a given node. + * @param node is the node index of a node in the graph. + * @returns an object of type T contained in the given node. + */ + auto getNodeValue(const size_t node) const -> const T& { + if (node < nNodes_) { + return nodeValues_[node]; + } + std::ostringstream oss; + oss << "Node Number out of range: " << node << " (nNodes_=" << nNodes_ + << ")"; + throw std::invalid_argument(oss.str()); + } + + /// @returns the number of nodes in the graph. + [[nodiscard]] auto size() const -> size_t { return nNodes_; } + + /** + * @brief Returns the successor nodes of a given node + * @param node is the index of a node in the graph + * @returns a vector containing the node indices of all nodes the passed + * node has outgoing edges to. + */ + [[nodiscard]] auto getAdjacent(const size_t node) const + -> const std::vector>& { + return adjacencies_.at(node); + } + }; + /** + * @brief Converts a schedule of operations into a directional acyclic graph + * (DAG), where each operation is a node and each edge represents a + * dependency. + * @details A circuit made up of U3-Gates (represented by layers of + * StructU3's) and CZ-Gates (represented by layers of two element arrays + * denoting control and target qubits) is transformed into a graph modeling + * the circuit and operational dependencies. Each node contains a std::variant + * containing either a StructU3 or an array representing a CZ-Gate. Edges + * between nodes mean that the destination node is dependent on the source + * node (e.g. that the operation of the source node must be executed before + * the one of the destination node). + * @param schedule is a pair of vectors containing layers of StructU3's + * representing U3-Gates and TwoQubitGateLayers. + * @param nQubits is the number of qubits in the scheduled circuit. + * @returns a DiGraph consisting of nodes containing either a StructU3 + * representation of U3-Gates of an array representation of CZ Gates. + */ + static auto + convertCircuitToDAG(const std::pair>, + std::vector>& schedule, + size_t nQubits) + -> DirectedGraph>>; + /** + * @brief Recursively finds the cheapest path to the start node of the + * subproblem graph from a set of leaf nodes. + * @param subproblemGraph is the subproblem graph to find the path in. + * @param currentNode is the node of the current function call. + * @param leafNodes is a set of nodes with no outgoing edges (aka. leaf + * nodes). + * @param memo is a map used to store previously computed paths and their + * costs to avoid redundant calculations. + * @returns a pair made up of a vector of the indices making up the cheapest + * path and the path's total cost (the sum of the maximal theta angles of each + * layer) + */ + static auto cheapestPathToStart( + const DirectedGraph, std::vector>>& + subproblemGraph, + size_t currentNode, const std::unordered_set& leafNodes, + std::unordered_map, qc::fp>>& memo) + -> std::pair, double>; + + /** + * @brief Finds the cheapest (lowest cost) path from the start node to a leaf + * node in a subproblemGraph. + * @param subproblemGraph is the subproblem graph. + * @param leafNodes is a vector containing the indices of all leaf nodes of + * the graph. + * @returns a vector containing the node indices of the cheapest path through + * the graph. + */ + static auto findCheapestPath( + const DirectedGraph, std::vector>>& + subproblemGraph, + const std::vector& leafNodes) -> std::vector; + + /** + * @brief Finds the leaf nodes (nodes with no outgoing edges) of a subproblem + * graph. + * @param subproblemGraph is the subproblem graph. + * @returns a vector of node indices for the leaf nodes. + */ + static auto findLeafNodes( + const DirectedGraph, std::vector>>& + subproblemGraph) -> std::vector; + + /** + * @brief Returns all plausible subsets of the current layers to be + * scheduled. + * @param circuit is the graph representation of the quantum circuit. + * @param currentSingleQubitGates is a vector containing the node indices of + * the current set of single-qubit gates. + * @param nextSubproblem is an array [twoQubitGates, singleQubitGates, + * remainingGates] containing vectors holding the node indices of the next set + * of two-qubit gates, single-qubit gates and all remaining gates. + * @param checkFinalCond is a bool deciding whether to check for a strict cost + * reduction. + * @returns a vector holding pairs of the possible next layers to be + * scheduled [currentSingleQubitGates, twoQubitGates, singleQubitGates, + * remainingGates] and the layers associated. + */ + static auto getPossibleLayers( + const DirectedGraph>>& + circuit, + const std::vector& currentSingleQubitGates, + const std::array, 3>& nextSubproblem, + bool checkFinalCond) + -> std::vector, 4>, qc::fp>>; + + /** + * @brief Finds the maximal value of the angle theta among the given set of + * nodes. + * @param circuit is the passed circuit graph containing operations. + * @param nodes is a vector of node indices for which to find the maximal + * theta. + * @returns the maximal theta value. + */ + static auto + maxTheta(const DirectedGraph>>& + circuit, + const std::vector& nodes) -> qc::fp; + + /** + * @brief returns the next two- and single-Qubit layers which can be + * scheduled. + * @param circuit is the quantum circuit in graph form. + * @param remainingNodes is a vector containing all unscheduled nodes. + * @param nQubits is the number of qubits in the circuit. + * @returns an array containing vectors of the next single-qubit gate and + * two-qubit gate layers which can be scheduled and the remaining nodes: + * [twoQubitGates, singleQubitGates, remainingGates] + */ + static auto + sift(const DirectedGraph>>& + circuit, + const std::vector& remainingNodes, size_t nQubits) + -> std::array, 3>; + + /** + * @brief Builds a schedule from a circuit and subproblem graph. + * @param circuit is the circuit to be scheduled in graph form. + * @param subproblemGraph is the subproblem graph of the circuit. + * @returns a pair of vectors containing layers of `StructU3`'s and two + * element arrays of qubits representing CZ gates making up a schedule. + */ + static auto buildSchedule( + const DirectedGraph>>& + circuit, + const DirectedGraph, std::vector>>& + subproblemGraph) -> std::pair>, + std::vector>; + + /** + * @brief Adds a node corresponding to the subproblem [twoQubitGates, + * singleQubitGates] to the subproblem graph. + * @param twoQubitGates is a vector of node indices making up a two-qubit gate + * layer. + * @param singleQubitGates is a vector of node indices making up a + * single-qubit gate layer. + * @param cost is the maximal theta value of operations in @p singleQubitGates + * (aka. the cost). + * @param subproblemGraph is a subproblem graph of a circuit. + * @param prevNode is the node corresponding to the previous subproblem. + * @returns the node index of the node added to the subproblem graph. + */ + static auto addNodeToSubproblemGraph( + const std::vector& twoQubitGates, + const std::vector& singleQubitGates, qc::fp cost, + DirectedGraph, std::vector>>& + subproblemGraph, + size_t prevNode) -> size_t; + + /** + * @brief Recursively creates a subproblem graph for a given circuit. + * @param subproblem is the current subproblem [twoQubitGates, + * singleQubitGates, remainingGates] for which to create a schedule. + * @param circuit is the graph representation of the circuit to be scheduled. + * @param subproblemGraph is the subproblem graph of the circuit to be + * scheduled. + * @param prevNode is the previous node in the subproblem graph. + * @param nQubits is the number of qubits in the circuit. + * @param checkFinalCond is a bool deciding whether the function should only + * allow possible next layers with strictly decreasing cost. + * @param memo is a map using subproblem hashes as keys and the actual + * subproblem as values. A subproblem is stored as a pair of a node index in + * the subproblem graph and an array containing the cost of the single-qubit + * layer in the current subproblem and the total cost of the schedule + * originating from that subproblem. + * @returns the cost of the schedule originating from the current subproblem. + */ + static auto scheduleRemaining( + const std::array, 3>& subproblem, + const DirectedGraph>>& + circuit, + DirectedGraph, std::vector>>& + subproblemGraph, + size_t prevNode, size_t nQubits, bool checkFinalCond, + std::unordered_map, 3>, + std::pair>, + SubproblemHasher>& memo) -> double; + + /** + * @brief Creates a schedule minimizing the total sum of the global rotation + * angles theta across a quantum circuit. + * @param schedule is the preliminary schedule. + * @param nQubits is the number of qubits in the circuit. + * @returns a schedule minimizing the total rotation angle theta + */ + [[nodiscard]] auto + scheduleThetaOpt(const std::pair>, + std::vector>& schedule, + size_t nQubits) const + -> std::pair>, + std::vector>; +}; +} // namespace na::zoned diff --git a/include/na/zoned/decomposer/NoOpDecomposer.hpp b/include/na/zoned/decomposer/NoOpDecomposer.hpp index d0a9edef1..e1a488ea8 100644 --- a/include/na/zoned/decomposer/NoOpDecomposer.hpp +++ b/include/na/zoned/decomposer/NoOpDecomposer.hpp @@ -48,8 +48,17 @@ class NoOpDecomposer : public DecomposerBase { NoOpDecomposer(const Architecture& /* unused */, const Config& /* unused */) { } + /** + * @brief Decomposes single-qubit and two-qubit gate layers by copying them + * unchanged (no-op decomposition). + * @param nQubits the number of qubits in the circuit. + * @param singleQubitGateLayers the single-qubit gate layers to decompose. + * @param twoQubitGateLayers the two-qubit gate layers of the schedule. + * @returns the decomposition result. + */ [[nodiscard]] auto - decompose(const std::vector& singleQubitGateLayers, + decompose(size_t nQubits, + const std::vector& singleQubitGateLayers, const std::vector& twoQubitGateLayers) const -> DecompositionResult override; }; diff --git a/python/mqt/qmap/na/zoned.pyi b/python/mqt/qmap/na/zoned.pyi index b0fb618bf..dcb96a91c 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -131,7 +131,7 @@ class RoutingAgnosticCompiler: The compilation result as a string in the .naviz format. """ - def stats(self) -> dict[str, float]: + def stats(self) -> dict[str, object]: """Get the statistics of the last compilation as a JSON-style dictionary. Returns: @@ -213,7 +213,93 @@ class RoutingAwareCompiler: The compilation result as a string in the .naviz format. """ - def stats(self) -> dict[str, float]: + def stats(self) -> dict[str, object]: + """Get the statistics of the last compilation. + + Returns: + The statistics as a dictionary + """ + +class RoutingAwareNativeGateCompiler: + """Routing-aware native gate zoned neutral atom compiler.""" + + def __init__( + self, + arch: ZonedNeutralAtomArchitecture, + log_level: str = "I", + max_filling_factor: float = 0.9, + theta_opt_schedule: bool = False, + check_final_cond: bool = False, + use_window: bool = True, + window_min_width: int = 16, + window_ratio: float = 1.0, + window_share: float = 0.8, + placement_method: PlacementMethod = ..., + deepening_factor: float = ..., + deepening_value: float = 0.0, + lookahead_factor: float = ..., + reuse_level: float = 5.0, + max_nodes: int = 10000000, + trials: int = 4, + queue_capacity: int = 100, + routing_method: RoutingMethod = ..., + prefer_split: float = 1.0, + warn_unsupported_gates: bool = True, + ) -> None: + """Create a routing-aware native gate compiler for the given architecture and configurations. + + Args: + arch: The zoned neutral atom architecture + log_level: The log level for the compiler, possible values are "debug"/"D", "info"/"I", "warning"/"W", "error"/"E", and "critical"/"C" + max_filling_factor: The maximum filling factor for the entanglement zone, i.e., it sets the limit for the maximum number of entangling gates that are scheduled in parallel + theta_opt_schedule: If this setting is turned on, a re-scheduling pass is executed immediately after translating the gates into their U3 representation. The theta optimization tries to minimize the maximum theta per layer by possibly scheduling single-qubit gates in later layers. + check_final_cond: If enabled the theta optimization checks if the sum of the resulting layer's maximum theta and the next layer's maximum theta is strictly less than the sum of previous maximum thetas. This does not guarantee that the total schedule is the one with minimal cost but reduces the recursive calls by excluding some subsets. + use_window: Whether to use a window for the placer + window_min_width: The minimum width of the window for the placer + window_ratio: The ratio between the height and the width of the window + window_share: The share of free sites in the window in relation to the number of atoms to be moved in this step + placement_method: The placement method that should be used for the heuristic placer + deepening_factor: Controls the impact of the term in the heuristic of the A* search that resembles the standard deviation of the differences between the current and target sites of the atoms to be moved in every orientation + deepening_value: Is added to the sum of standard deviations before it is multiplied with the number of unplaced nodes and :attr:`deepening_factor` + lookahead_factor: Controls the lookahead's influence that considers the distance of atoms to their interaction partner in the next layer + reuse_level: The reuse level that corresponds to the estimated extra fidelity loss due to the extra trap transfers when the atom is not reused and instead moved to the storage zone and back to the entanglement zone + max_nodes: The maximum number of nodes that are considered in the A* search. + If this number is exceeded, the search is aborted and an error is raised. + In the current implementation, one node roughly consumes 120 Byte. + Hence, allowing 50,000,000 nodes results in memory consumption of about 6 GB plus the size of the rest of the data structures. + trials: The number of restarts during IDS. + queue_capacity: The maximum capacity of the priority queue used during IDS. + routing_method: The routing method that should be used for the independent set router + prefer_split: The threshold factor for group merging decisions during routing. + warn_unsupported_gates: Whether to warn about unsupported gates in the code generator + """ + + @staticmethod + def from_json_string(arch: ZonedNeutralAtomArchitecture, json: str) -> RoutingAwareNativeGateCompiler: + """Create a compiler for the given architecture and configurations from a JSON string. + + Args: + arch: The zoned neutral atom architecture + json: The JSON string + + Returns: + The initialized compiler + + Raises: + ValueError: If the string is not a valid JSON string + """ + + def compile(self, qc: mqt.core.ir.QuantumComputation) -> str: + """Compile a quantum circuit for the zoned neutral atom architecture. + + Args: + qc: The quantum circuit + + Returns: + The compilation result as a string in the .naviz format. + """ + + def stats(self) -> dict[str, object]: """Get the statistics of the last compilation. Returns: diff --git a/src/na/zoned/decomposer/NativeGateDecomposer.cpp b/src/na/zoned/decomposer/NativeGateDecomposer.cpp new file mode 100644 index 000000000..73b4f4d4d --- /dev/null +++ b/src/na/zoned/decomposer/NativeGateDecomposer.cpp @@ -0,0 +1,729 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "na/zoned/decomposer/NativeGateDecomposer.hpp" + +#include "ir/operations/CompoundOperation.hpp" +#include "ir/operations/Operation.hpp" +#include "ir/operations/StandardOperation.hpp" +#include "spdlog/spdlog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace na::zoned { +namespace { +/** + * Helper to combine multiple lambdas into a single overload set for + * `std::visit`. + */ +template struct overloads : Ts... { + using Ts::operator()...; +}; +} // namespace +NativeGateDecomposer::NativeGateDecomposer(const Architecture&, + const Config& config) + : config_(config) {} +auto NativeGateDecomposer::convertGateToQuaternion( + const std::reference_wrapper op) -> Quaternion { + assert(op.get().getNqubits() == 1 && "Works only for single-qubit gates."); + switch (op.get().getType()) { + case qc::RZ: + case qc::P: + return {cos(op.get().getParameter().front() / 2), 0, 0, + sin(op.get().getParameter().front() / 2)}; + case qc::Z: + return {0, 0, 0, 1}; + case qc::S: + return {cos(qc::PI_4), 0, 0, sin(qc::PI_4)}; + case qc::Sdg: + return {cos(-qc::PI_4), 0, 0, sin(-qc::PI_4)}; + case qc::T: + return {cos(qc::PI_4 / 2), 0, 0, sin(qc::PI_4 / 2)}; + case qc::Tdg: + return {cos(-qc::PI_4 / 2), 0, 0, sin(-qc::PI_4 / 2)}; + case qc::U: + return combineQuaternions( + combineQuaternions({cos(op.get().getParameter().at(1) / 2), 0, 0, + sin(op.get().getParameter().at(1) / 2)}, + {cos(op.get().getParameter().front() / 2), 0, + sin(op.get().getParameter().front() / 2), 0}), + {cos(op.get().getParameter().at(2) / 2), 0, 0, + sin(op.get().getParameter().at(2) / 2)}); + case qc::U2: + return combineQuaternions( + combineQuaternions({cos(op.get().getParameter().front() / 2), 0, 0, + sin(op.get().getParameter().front() / 2)}, + {cos(qc::PI_4), 0, sin(qc::PI_4), 0}), + {cos(op.get().getParameter().at(1) / 2), 0, 0, + sin(op.get().getParameter().at(1) / 2)}); + case qc::RX: + return {cos(op.get().getParameter().front() / 2), + sin(op.get().getParameter().front() / 2), 0, 0}; + case qc::RY: + return {cos(op.get().getParameter().front() / 2), 0, + sin(op.get().getParameter().front() / 2), 0}; + case qc::H: + return combineQuaternions( + combineQuaternions({1, 0, 0, 0}, {cos(qc::PI_4), 0, sin(qc::PI_4), 0}), + {cos(qc::PI_2), 0, 0, sin(qc::PI_2)}); + case qc::X: + return {0, 1, 0, 0}; + case qc::Y: + return {0, 0, 1, 0}; + case qc::Vdg: + return combineQuaternions( + combineQuaternions({cos(qc::PI_4), 0, 0, sin(qc::PI_4)}, + {cos(-qc::PI_4), 0, sin(-qc::PI_4), 0}), + {cos(-qc::PI_4), 0, 0, sin(-qc::PI_4)}); + case qc::SX: + return combineQuaternions( + combineQuaternions({cos(-qc::PI_4), 0, 0, sin(-qc::PI_4)}, + {cos(qc::PI_4), 0, sin(qc::PI_4), 0}), + {cos(qc::PI_4), 0, 0, sin(qc::PI_4)}); + case qc::SXdg: + case qc::V: + return combineQuaternions( + combineQuaternions({cos(-qc::PI_4), 0, 0, sin(-qc::PI_4)}, + {cos(-qc::PI_4), 0, sin(-qc::PI_4), 0}), + {cos(qc::PI_4), 0, 0, sin(qc::PI_4)}); + default: + std::ostringstream oss; + oss << "Unsupported single-qubit gate: " << op.get().getType(); + throw std::invalid_argument(oss.str()); + } +} + +auto NativeGateDecomposer::combineQuaternions(const Quaternion& q1, + const Quaternion& q2) + -> Quaternion { + return {q1.a * q2.a - q1.b * q2.b - q1.c * q2.c - q1.d * q2.d, + q1.a * q2.b + q1.b * q2.a + q1.c * q2.d - q1.d * q2.c, + q1.a * q2.c - q1.b * q2.d + q1.c * q2.a + q1.d * q2.b, + q1.a * q2.d + q1.b * q2.c - q1.c * q2.b + q1.d * q2.a}; +} + +auto NativeGateDecomposer::getU3AnglesFromQuaternion(const Quaternion& quat) + -> Angles { + Angles angles; + if (std::fabs(quat.a) > epsilon || std::fabs(quat.d) > epsilon) { + angles.theta = + 2. * std::atan2(std::sqrt(quat.c * quat.c + quat.b * quat.b), + std::sqrt(quat.a * quat.a + quat.d * quat.d)); + const qc::fp alpha1 = std::atan2(quat.d, quat.a); // (phi+ lambda) /2 + if (std::fabs(quat.b) > epsilon || std::fabs(quat.c) > epsilon) { + const qc::fp alpha2 = -1 * std::atan2(quat.b, quat.c); //(phi-lambda)/2 + angles.phi = alpha1 + alpha2; // phi + angles.lambda = alpha1 - alpha2; + } else { + angles.phi = 0; + angles.lambda = 2 * alpha1; + } + } else { + angles.theta = qc::PI; + if (std::fabs(quat.b) > epsilon || std::fabs(quat.c) > epsilon) { + angles.phi = 0; + angles.lambda = 2 * std::atan2(quat.b, quat.c); + } else { + throw std::invalid_argument("Invalid quaternion"); + } + } + return angles; +} + +auto NativeGateDecomposer::calcThetaMax(const std::vector& layers) + -> qc::fp { + assert(!layers.empty() && "Empty layer."); + const auto thetas = + layers | std::views::transform([](const auto& gate) -> qc::fp { + return std::fabs(gate.angles.theta); + }); + return *std::ranges::max_element(thetas); +} +auto NativeGateDecomposer::transformToU3( + const std::vector& layers, const size_t nQubits) + -> std::vector> { + std::vector> newLayers; + for (const auto& layer : layers) { + std::vector>> + gatesPerQubit(nQubits); + std::ranges::for_each(layer, [&gatesPerQubit](const auto& gate) -> void { + // if compound operations, go instead over the contained operations + if (gate.get().isCompoundOperation()) { + const auto& compoundOp = + dynamic_cast(gate.get()); + std::ranges::for_each( + compoundOp, [&gatesPerQubit](const auto& subGate) -> void { + assert(subGate->getNqubits() == 1 && + "Gate has to be a single qubit gate, in particular, no " + "nested compound operations are allowed."); + gatesPerQubit[subGate->getTargets().front()].emplace_back( + *subGate); + }); + } else { + assert(gate.get().getNqubits() == 1 && + "Gate has to be a single qubit gate."); + gatesPerQubit[gate.get().getTargets().front()].emplace_back(gate); + } + }); + auto& newLayer = newLayers.emplace_back(); + std::ranges::transform( + std::views::iota(0UL, gatesPerQubit.size()) | + std::views::filter([&gatesPerQubit](const auto i) -> bool { + return !gatesPerQubit[i].empty(); + }), + std::back_inserter(newLayer), [&gatesPerQubit](const auto i) -> U3Gate { + const auto& gates = gatesPerQubit[i]; + const auto& quat = std::accumulate( + gates.begin(), gates.end(), Quaternion{}, + [](const Quaternion& q, const auto& gate) -> Quaternion { + return combineQuaternions(q, convertGateToQuaternion(gate)); + }); + const auto& angles = getU3AnglesFromQuaternion(quat); + return U3Gate{.angles = angles, + .qubit = gates.front().get().getTargets().front()}; + }); + } + return newLayers; +} +auto NativeGateDecomposer::getDecompositionAngles(const Angles& angles, + const qc::fp thetaMax) + -> Angles { + qc::fp alpha; + Angles decompAngles; + // U3(theta,phi_min(phi),phi_plus(lambda))->Rz(gamma_minus)GR(theta_max/2, + // PI_2)Rz(chi)GR(-theta_max/2,PI_2)RZ(gamma_plus) + const auto sinSquareDiff = sin(thetaMax / 2) * sin(thetaMax / 2) - + sin(angles.theta / 2) * sin(angles.theta / 2); + if (std::fabs(sinSquareDiff) < epsilon) { + decompAngles.theta = qc::PI; + if (std::fabs(cos(thetaMax / 2)) < epsilon) { + alpha = 0; + } else { + alpha = qc::PI_2; + } + } else { + const auto kappa = std::sqrt( + (sin(angles.theta / 2) * sin(angles.theta / 2)) / sinSquareDiff); + alpha = atan(cos(thetaMax / 2) * kappa); + decompAngles.theta = fmod(2 * atan(kappa), qc::TAU); + } + const auto beta = angles.theta < 0 ? -1 * qc::PI_2 : qc::PI_2; + // gamma_plus + decompAngles.lambda = fmod(angles.lambda - (alpha + beta), qc::TAU); + // gamma_minus + decompAngles.phi = fmod(angles.phi - (alpha - beta), qc::TAU); + return decompAngles; +} + +auto NativeGateDecomposer::decompose( + const size_t nQubits, + const std::vector& singleQubitGateLayers, + const std::vector& twoQubitGateLayers) const + -> DecompositionResult { + auto u3Layers = transformToU3(singleQubitGateLayers, nQubits); + std::vector newTwoQubitLayers; + if (config_.thetaOptSchedule) { + auto [optSingleQubitGateLayers, optTwoQubitGateLayers] = + scheduleThetaOpt(std::pair(u3Layers, twoQubitGateLayers), nQubits); + u3Layers = std::move(optSingleQubitGateLayers); + newTwoQubitLayers = std::move(optTwoQubitGateLayers); + } else { + newTwoQubitLayers = twoQubitGateLayers; + } + std::vector newSingleQubitLayers; + for (const auto& layer : u3Layers) { + auto& newLayer = newSingleQubitLayers.emplace_back(); + if (!layer.empty()) { + const auto thetaMax = calcThetaMax(layer); + SingleQubitGateLayer frontLayer; + SingleQubitGateLayer midLayer; + SingleQubitGateLayer backLayer; + + for (auto gate : layer) { + const auto& [theta, phi, lambda] = + getDecompositionAngles(gate.angles, thetaMax); + frontLayer.emplace_back(std::make_unique( + gate.qubit, qc::RZ, std::vector{phi})); + midLayer.emplace_back(std::make_unique( + gate.qubit, qc::RZ, std::vector{theta})); + backLayer.emplace_back(std::make_unique( + gate.qubit, qc::RZ, std::vector{lambda})); + } + std::vector> globalRotation; + std::vector> globalReversRotation; + for (size_t i = 0; i < nQubits; ++i) { + globalRotation.emplace_back(std::make_unique( + i, qc::RY, std::vector{thetaMax / 2})); + globalReversRotation.emplace_back( + std::make_unique( + i, qc::RY, std::vector{-thetaMax / 2})); + } + // combine all lists into a flat list + std::ranges::move(frontLayer, std::back_inserter(newLayer)); + newLayer.emplace_back(std::make_unique( + std::move(globalRotation), true)); + std::ranges::move(midLayer, std::back_inserter(newLayer)); + newLayer.emplace_back(std::make_unique( + std::move(globalReversRotation), true)); + std::ranges::move(backLayer, std::back_inserter(newLayer)); + } + } + return {.singleQubitLayers = std::move(newSingleQubitLayers), + .twoQubitLayers = std::move(newTwoQubitLayers)}; +} + +auto NativeGateDecomposer::findCheapestPath( + const DirectedGraph, + std::vector>>& subproblemGraph, + const std::vector& leafNodes) -> std::vector { + const std::unordered_set leaves(leafNodes.begin(), leafNodes.end()); + // Memory map: Subproblem nodes as keys + std::unordered_map, qc::fp>> memo; + auto [path, cost] = cheapestPathToStart(subproblemGraph, 0, leaves, memo); + path.resize(path.size() - 1); + std::ranges::reverse(path); + return path; +} +namespace { +/// Returns true if set1 and set2 share no elements. +template +auto disjunct(const std::unordered_set& set1, + const std::unordered_set& set2) -> bool { + return std::ranges::all_of( + set1, [&set2](const T& elem) -> bool { return !set2.contains(elem); }); +} +} // namespace + +auto NativeGateDecomposer::cheapestPathToStart( + const DirectedGraph, + std::vector>>& subproblemGraph, + std::size_t currentNode, const std::unordered_set& leafNodes, + std::unordered_map, qc::fp>>& memo) + -> std::pair, double> { + std::vector, double>> possiblePaths; + // Check the memoization map + if (memo.contains(currentNode)) { + return memo.at(currentNode); + } + // Base case + for (const auto [target, cost] : subproblemGraph.getAdjacent(currentNode)) { + if (leafNodes.contains(target)) { + possiblePaths.emplace_back(std::vector{target, currentNode}, cost); + } + } + // Recursive case + if (possiblePaths.empty()) { + for (auto [target, cost] : subproblemGraph.getAdjacent(currentNode)) { + auto [path, accCost] = + cheapestPathToStart(subproblemGraph, target, leafNodes, memo); + path.emplace_back(currentNode); + possiblePaths.emplace_back(path, accCost + cost); + } + } + // Choose the cheapest path + assert(!possiblePaths.empty() && "No path found to leaf nodes."); + const auto& bestPathWithCost = *std::ranges::min_element( + possiblePaths, + [](const auto& a, const auto& b) -> bool { return a.second < b.second; }); + memo[currentNode] = bestPathWithCost; + return bestPathWithCost; +} + +auto NativeGateDecomposer::findLeafNodes( + const DirectedGraph, + std::vector>>& subproblemGraph) + -> std::vector { + std::vector leafNodes; + std::ranges::copy(std::views::iota(0UL, subproblemGraph.size()) | + std::views::filter([&subproblemGraph](auto i) -> bool { + return subproblemGraph.getAdjacent(i).empty(); + }), + std::back_inserter(leafNodes)); + return leafNodes; +} + +auto NativeGateDecomposer::getPossibleLayers( + const DirectedGraph>>& + circuit, + const std::vector& currentSingleQubitGates, + const std::array, 3>& nextSubproblem, + bool checkFinalCond) + -> std::vector, 4>, qc::fp>> { + + auto vP1Star = nextSubproblem[0]; + auto vc1Star = nextSubproblem[1]; + std::vector vP1Square; + std::vector vc1Square; + + auto vc0Cost = maxTheta(circuit, currentSingleQubitGates); + auto vc1Cost = maxTheta(circuit, nextSubproblem[1]); + auto origCombCost = vc0Cost + vc1Cost; + auto newVc1Cost = std::max(vc0Cost, vc1Cost); + + std::array vArg{currentSingleQubitGates, nextSubproblem[0], nextSubproblem[1], + nextSubproblem[2]}; + std::vector args{std::pair(vArg, vc0Cost)}; + if (currentSingleQubitGates.empty()) { + return args; + } + // Sort currentSingleQubitGates from highest to lowest theta + std::vector vSort(currentSingleQubitGates); + std::ranges::sort( + vSort, std::greater{}, [&circuit](const auto& gate) -> double { + return std::fabs( + std::get(circuit.getNodeValue(gate)).angles.theta); + }); + // Check Condition 1 + std::vector, 2>, + std::pair, qc::fp>>> + potentialArg; + auto prevTheta = + std::fabs(std::get(circuit.getNodeValue(vSort[0])).angles.theta); + double thisTheta; + std::unordered_set mkQubits{ + std::get(circuit.getNodeValue(vSort[0])).qubit}; + for (size_t i = 0; i < vSort.size(); i++) { + thisTheta = std::fabs( + std::get(circuit.getNodeValue(vSort[i])).angles.theta); + if (thisTheta != prevTheta) { + std::vector discarded(vSort.begin(), + vSort.begin() + static_cast(i)); + std::vector kept(vSort.begin() + static_cast(i), vSort.end()); + potentialArg.emplace_back(std::array{kept, discarded}, + std::pair{mkQubits, thisTheta}); + prevTheta = thisTheta; + mkQubits.clear(); + } + mkQubits.insert(std::get(circuit.getNodeValue(vSort[i])).qubit); + } + std::vector emplaceBackNodes; + std::unordered_set pSquareQubits; + + for (auto pot : potentialArg) { + // Check Condition 2 + emplaceBackNodes.clear(); + for (auto node : vP1Star) { + std::unordered_set qubits = { + std::get>(circuit.getNodeValue(node))[0], + std::get>(circuit.getNodeValue(node))[1]}; + if (!disjunct(qubits, pot.second.first)) { + emplaceBackNodes.emplace_back(node); + pSquareQubits.merge(qubits); + } + } + for (auto node : emplaceBackNodes) { + const auto ret = std::ranges::remove(vP1Star, node); + vP1Star.erase(ret.begin(), ret.end()); + vP1Square.emplace_back(node); + } + + if (vP1Star.empty()) { + break; + } + // Check Condition 3 + std::unordered_set pushQubits = pot.second.first; + pushQubits.insert(pSquareQubits.begin(), pSquareQubits.end()); + emplaceBackNodes.clear(); + + for (auto node : vc1Star) { + std::unordered_set qubits = { + std::get(circuit.getNodeValue(node)).qubit}; + if (!disjunct(qubits, pushQubits)) { + emplaceBackNodes.emplace_back(node); + } + } + for (auto node : emplaceBackNodes) { + std::erase(vc1Star, node); + vc1Square.emplace_back(node); + } + + if (vc1Star.empty()) { + break; + } + // Check Condition 4 + if (!checkFinalCond || pot.second.second + newVc1Cost < origCombCost) { + vArg = {pot.first[0], vP1Star, pot.first[1], nextSubproblem[2]}; + vArg[2].insert(vArg[2].end(), vc1Star.begin(), vc1Star.end()); + vArg[3].insert(vArg[3].end(), vc1Square.begin(), vc1Square.end()); + vArg[3].insert(vArg[3].end(), vP1Square.begin(), vP1Square.end()); + args.emplace_back(vArg, pot.second.second); + } + } + return args; +} + +auto NativeGateDecomposer::convertCircuitToDAG( + const std::pair>, + std::vector>& schedule, + const std::size_t nQubits) + -> DirectedGraph>> { + // std::variant> instead of + // Unique_pointer For Readout: + DirectedGraph>> graph; + std::vector> qubitPaths(nQubits); + // TODO:assert that One more sql exists than mql ?? + for (size_t i = 0; i < schedule.second.size(); ++i) { + for (const auto& s : schedule.first.at(i)) { + size_t node = graph.addNode(s); + qubitPaths.at(s.qubit).emplace_back(node); + } + + for (const auto& gate : schedule.second.at(i)) { + size_t node = graph.addNode(gate); + qubitPaths.at(gate[0]).emplace_back(node); + qubitPaths.at(gate[1]).emplace_back(node); + } + } + SPDLOG_DEBUG("Added Nodes"); + for (const auto& s : schedule.first.back()) { + size_t node = graph.addNode(s); + qubitPaths.at(s.qubit).emplace_back(node); + } + for (std::size_t i = 0; i < qubitPaths.size(); ++i) { + if (qubitPaths.at(i).size() > 0) { + for (std::size_t op = 0; op < (qubitPaths.at(i).size() - 1); ++op) { + graph.addEdge(qubitPaths.at(i).at(op), qubitPaths.at(i).at(op + 1)); + } + } + } + return graph; +} + +auto NativeGateDecomposer::maxTheta( + const DirectedGraph>>& + circuit, + const std::vector& nodes) -> qc::fp { + qc::fp max_cost = 0; + for (const auto node : nodes) { + if (std::fabs(std::get(circuit.getNodeValue(node)).angles.theta) >= + max_cost) { + max_cost = + std::fabs(std::get(circuit.getNodeValue(node)).angles.theta); + } + } + return max_cost; +} +auto NativeGateDecomposer::sift( + const DirectedGraph>>& + circuit, + const std::vector& remainingNodes, size_t nQubits) + -> std::array, 3> { + std::vector twoQubitGates; + std::vector singleQubitGates; + std::vector remainingGates; + + // we use a sorted set here on purpose to align with the topological order + // of the DAG, see also the next for loop. + std::set vRemaining(remainingNodes.begin(), remainingNodes.end()); + std::unordered_set removed; + + // the remainingNodes ids already reflect the graph's topological order, so + // sorting is enough to iterate nodes in topological order. + for (const auto node : vRemaining) { + auto op = circuit.getNodeValue(node); + if (const auto opQubits = std::visit( + overloads{[](const U3Gate& u3) -> std::unordered_set { + return {u3.qubit}; + }, + [](const std::array& cz) + -> std::unordered_set { + return {cz[0], cz[1]}; + }}, + op); + removed.size() < nQubits && disjunct(removed, opQubits)) { + std::visit( + overloads{ + [&singleQubitGates, &removed, node](const U3Gate& u3) -> void { + singleQubitGates.emplace_back(node); + removed.emplace(u3.qubit); + }, + [&twoQubitGates, node](const std::array&) -> void { + twoQubitGates.emplace_back(node); + }}, + op); + } else { + remainingGates.emplace_back(node); + std::ranges::copy(opQubits, std::inserter(removed, removed.end())); + } + } + return {{twoQubitGates, singleQubitGates, remainingGates}}; +} + +auto NativeGateDecomposer::buildSchedule( + const DirectedGraph>>& + circuit, + const DirectedGraph, + std::vector>>& subproblemGraph) + -> std::pair>, + std::vector> { + + const auto& leafNodes = findLeafNodes(subproblemGraph); + const auto& minimalPath = findCheapestPath(subproblemGraph, leafNodes); + std::pair>, std::vector> + schedule; + + std::vector singleQubitGates; + std::vector> twoQubitGates; + + if (!subproblemGraph.getNodeValue(minimalPath[0]).first.empty()) { + schedule.first.emplace_back(); + } + std::unordered_set usedQubits; + for (std::size_t i = 0; i < minimalPath.size(); i++) { + singleQubitGates.clear(); + twoQubitGates.clear(); + usedQubits.clear(); + for (const auto j : subproblemGraph.getNodeValue(minimalPath[i]).first) { + if (const auto& op = circuit.getNodeValue(j); + std::holds_alternative>(op)) { + // Check if two-qubit gates can be executed in parallel + const auto& gate = std::get>(op); + if (usedQubits.contains(gate[0]) || usedQubits.contains(gate[1])) { + schedule.second.emplace_back(twoQubitGates); + schedule.first.emplace_back(); + twoQubitGates.clear(); + usedQubits.clear(); + } + usedQubits.insert(gate[0]); + usedQubits.insert(gate[1]); + twoQubitGates.emplace_back(gate); + } + } + + for (const auto j : subproblemGraph.getNodeValue(minimalPath[i]).second) { + if (const auto& op = circuit.getNodeValue(j); + std::holds_alternative(op)) { + singleQubitGates.emplace_back(std::get(op)); + } + } + schedule.first.emplace_back(singleQubitGates); + if (i != 0 || !subproblemGraph.getNodeValue(minimalPath[0]).first.empty()) { + schedule.second.emplace_back(twoQubitGates); + } + } + return schedule; +} + +auto NativeGateDecomposer::addNodeToSubproblemGraph( + const std::vector& twoQubitGates, + const std::vector& singleQubitGates, const qc::fp cost, + DirectedGraph, + std::vector>>& subproblemGraph, + const std::size_t prevNode) -> size_t { + const auto newNode = + subproblemGraph.addNode(std::pair(twoQubitGates, singleQubitGates)); + subproblemGraph.addEdge(prevNode, newNode, cost); + return newNode; +} + +auto NativeGateDecomposer::scheduleRemaining( + const std::array, 3>& subproblem, + const DirectedGraph>>& + circuit, + DirectedGraph, std::vector>>& + subproblemGraph, + const size_t prevNode, const size_t nQubits, const bool checkFinalCond, + std::unordered_map, 3>, + std::pair>, + SubproblemHasher>& memo) -> double { + double cost; + // Check if a subproblem has been computed + if (memo.contains(subproblem)) { + const auto [to, result] = memo.at(subproblem); + cost = result[0]; + subproblemGraph.addEdge(prevNode, to, result[1]); + return cost; + } + // Base Case: remaining nodes is empty + if (subproblem[2].empty()) { + if (subproblem[1].empty()) { + cost = 0; + } else { + cost = + std::fabs(std::get(circuit.getNodeValue(subproblem[1].at(0))) + .angles.theta); + } + for (const auto i : subproblem[1]) { + if (std::fabs(std::get(circuit.getNodeValue(i)).angles.theta) > + cost) { + cost = + std::fabs(std::get(circuit.getNodeValue(i)).angles.theta); + } + } + const auto endNode = addNodeToSubproblemGraph( + subproblem[0], subproblem[1], cost, subproblemGraph, prevNode); + memo[subproblem] = + std::pair>(endNode, {cost, cost}); + return cost; + } + // Recursive call + const auto& nextSubproblem = sift(circuit, subproblem[2], nQubits); + const auto& args = + getPossibleLayers(circuit, subproblem[1], nextSubproblem, checkFinalCond); + assert(!args.empty() && "No possible layers found."); + qc::fp tempCost = 0.0; + auto minCost = std::numeric_limits::max(); + auto minWeight = std::numeric_limits::max(); + std::size_t minNode; + for (const auto& [singleQubitGates, nodeCost] : args) { + const auto newNode = + addNodeToSubproblemGraph(subproblem[0], singleQubitGates[0], nodeCost, + subproblemGraph, prevNode); + tempCost = + scheduleRemaining( + {singleQubitGates[1], singleQubitGates[2], singleQubitGates[3]}, + circuit, subproblemGraph, newNode, nQubits, checkFinalCond, memo) + + nodeCost; + if (tempCost < minCost) { + minCost = tempCost; + minNode = newNode; + minWeight = nodeCost; + } + } + memo[subproblem] = {minNode, {minCost, minWeight}}; + return minCost; +} + +auto NativeGateDecomposer::scheduleThetaOpt( + const std::pair>, + std::vector>& schedule, + const std::size_t nQubits) const + -> std::pair>, + std::vector> { + // Convert circuit to DAG + auto circuit = convertCircuitToDAG(schedule, nQubits); + // Get initial layers + std::vector allNodes(circuit.size()); + std::iota(allNodes.begin(), allNodes.end(), 0); + const auto& subproblem = sift(circuit, allNodes, nQubits); + // Create subproblem graph + DirectedGraph, std::vector>> + subproblemGraph; + // First call of recursive function to create schedule + const auto baseNode = subproblemGraph.addNode({}); + std::unordered_map, 3>, + std::pair>, + SubproblemHasher> + memo; + scheduleRemaining(subproblem, circuit, subproblemGraph, baseNode, nQubits, + config_.checkFinalCond, memo); + // Create a schedule from the subproblem graph + return buildSchedule(circuit, subproblemGraph); +} +} // namespace na::zoned diff --git a/src/na/zoned/decomposer/NoOpDecomposer.cpp b/src/na/zoned/decomposer/NoOpDecomposer.cpp index 3d2e63dbd..e4fa3401c 100644 --- a/src/na/zoned/decomposer/NoOpDecomposer.cpp +++ b/src/na/zoned/decomposer/NoOpDecomposer.cpp @@ -17,6 +17,7 @@ namespace na::zoned { auto NoOpDecomposer::decompose( + [[maybe_unused]] const size_t nQubits, const std::vector& singleQubitGateLayers, const std::vector& twoQubitGateLayers) const -> DecompositionResult { diff --git a/test/na/zoned/CMakeLists.txt b/test/na/zoned/CMakeLists.txt index 72baf51ad..3ecbdb24f 100644 --- a/test/na/zoned/CMakeLists.txt +++ b/test/na/zoned/CMakeLists.txt @@ -8,10 +8,13 @@ if(TARGET MQT::QMapNAZoned) file(GLOB SOURCES *.cpp) + file(GLOB HEADERS *.hpp) file(GLOB CIRCUITS circuits/*.qasm) # make list of files a comma separated list of strings string(REPLACE ";" "\",\"" CIRCUITS "\"${CIRCUITS}\"") package_add_test(mqt-qmap-na-zoned-test MQT::QMapNAZoned ${SOURCES}) + target_sources(mqt-qmap-na-zoned-test PRIVATE FILE_SET HEADERS BASE_DIRS ${CMAKE_SOURCE_DIR}/test + FILES ${HEADERS}) target_link_libraries(mqt-qmap-na-zoned-test PRIVATE MQT::CoreCircuitOptimizer MQT::CoreQASM) target_compile_definitions(mqt-qmap-na-zoned-test PRIVATE TEST_CIRCUITS=${CIRCUITS}) endif() diff --git a/test/na/zoned/matcher.hpp b/test/na/zoned/matcher.hpp new file mode 100644 index 000000000..b16355a99 --- /dev/null +++ b/test/na/zoned/matcher.hpp @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "ir/operations/CompoundOperation.hpp" +#include "na/zoned/decomposer/NativeGateDecomposer.hpp" + +#include + +// NOLINTBEGIN(modernize-use-trailing-return-type) + +namespace testing { +/** + * @brief Computes the wraparound-aware absolute angular distance between two + * angles (radians), normalized into [-π, π]. + * @param a first angle. + * @param b second angle. + * @returns the absolute angular distance. + */ +constexpr auto angleDiff = [](const double a, const double b) -> double { + double d = std::fmod(a - b, 2 * qc::PI); + if (d > qc::PI) { + d -= 2 * qc::PI; + } + if (d < -qc::PI) { + d += 2 * qc::PI; + } + return std::fabs(d); +}; +/** + * @brief Matcher that checks if three Euler angles (theta, phi, lambda) + * are within a given tolerance of the expected angles. + * + * @param expected The expected na::zoned::NativeGateDecomposer::Angles value. + * @param tolerance Numeric tolerance for angle comparisons (radians). + */ +MATCHER_P2(AnglesNear, expected, tolerance, + std::string("angles ") + (negation ? "aren't" : "are") + + " near expected") { + if (angleDiff(arg.theta, expected.theta) <= tolerance && + angleDiff(arg.phi, expected.phi) <= tolerance && + angleDiff(arg.lambda, expected.lambda) <= tolerance) { + return true; + } + *result_listener << "actual: {theta=" << arg.theta << ", phi=" << arg.phi + << ", lambda=" << arg.lambda << "} " + << "expected: {theta=" << expected.theta + << ", phi=" << expected.phi << ", lambda=" << expected.lambda + << "} " + << "(tolerance=" << tolerance << ")"; + return false; +} +/** + * @brief Matcher for U3 gate-like structures: checks angles and target qubit. + * + * @param expected Struct containing expected angles and qubit. + * @param tolerance Tolerance passed to AnglesNear for angle comparisons. + */ +MATCHER_P2(U3GateNear, expected, tolerance, + DescribeMatcher( + AnglesNear(expected.angles, tolerance), negation) + + (negation ? " or " : " and ") + + DescribeMatcher(Eq(expected.qubit), negation)) { + return ExplainMatchResult(AnglesNear(expected.angles, tolerance), arg.angles, + result_listener) && + ExplainMatchResult(Eq(expected.qubit), arg.qubit, result_listener); +} +// Note: q and -q encode the same rotation, but the matcher only checks one +// sign. Currently, we have not implemented a canonicalized form, so equivalent +// results can fail here (but, currently, there are no false-positives). +/** + * @brief Matcher that compares two quaternions element-wise within tolerance. + * + * @param expected Quaternion with components {a, b, c, d} to compare against. + * @param tolerance Absolute tolerance for each component comparison. + */ +MATCHER_P2(QuaternionNear, expected, tolerance, + std::string("quaternion ") + (negation ? "isn't" : "is") + + " near expected") { + if (std::fabs(arg.a - expected.a) <= tolerance && + std::fabs(arg.b - expected.b) <= tolerance && + std::fabs(arg.c - expected.c) <= tolerance && + std::fabs(arg.d - expected.d) <= tolerance) { + return true; + } + *result_listener << "actual: {a=" << arg.a << ", b=" << arg.b + << ", c=" << arg.c << ", d=" << arg.d << "} " + << "expected: {a=" << expected.a << ", b=" << expected.b + << ", c=" << expected.c << ", d=" << expected.d << "} " + << "(tolerance=" << tolerance << ")"; + return false; +} +/** + * @brief Matcher verifying a rotation gate's type, target qubit, and angle. + * + * @param type Expected gate type (e.g., qc::RZ). + * @param qubit Expected target qubit index. + * @param angle Expected rotation angle (radians). + * @param tolerance Allowed difference between expected and actual angle. + */ +MATCHER_P4(ExpectRotationGate, type, qubit, angle, tolerance, + DescribeMatcher(Eq(type), negation) + + (negation ? " or " : " and ") + + DescribeMatcher(ElementsAre(Eq(qubit)), negation) + + (negation ? " or " : " and ") + + DescribeMatcher>( + ElementsAre(DoubleNear(angle, tolerance)), negation)) { + return ExplainMatchResult(Eq(type), arg->getType(), result_listener) && + ExplainMatchResult(ElementsAre(Eq(qubit)), arg->getTargets(), + result_listener) && + ExplainMatchResult(ElementsAre(DoubleNear(angle, tolerance)), + arg->getParameter(), result_listener); +} +/** + * @brief Matcher verifying a global rotation gate's type and angle. + * + * @param type Expected gate type (e.g., qc::RZ). + * @param nQubit Expected number of qubits the gate acts on. + * @param angle Expected rotation angle (radians). + * @param tolerance Allowed difference between expected and actual angle. + */ +MATCHER_P4(ExpectGlobalRotationGate, type, nQubit, angle, tolerance, + std::string("global gate ") + (negation ? "isn't" : "is") + + " as expected") { + if (arg->getType() != qc::Compound) { + *result_listener << "actual: type=" << arg->getType() + << ", expected: type=" << qc::Compound; + return false; + } + const auto& compoundOp = dynamic_cast(*arg); + if (compoundOp.size() != nQubit) { + *result_listener << "actual: nqubits=" << compoundOp.size() + << ", expected: nqubits=" << nQubit; + return false; + } + const auto& ops = compoundOp.getOps(); + for (size_t i = 0; i < nQubit; ++i) { + if (ops.at(i)->getType() != type) { + *result_listener << "actual: gate[" << i + << "] type=" << ops.at(i)->getType() + << ", expected: type=" << type; + return false; + } + if (ops.at(i)->getTargets().front() != i) { + *result_listener << "actual: gate[" << i + << "] target=" << ops.at(i)->getTargets().front() + << ", expected: target=" << i; + return false; + } + if (std::fabs(ops.at(i)->getParameter().front() - angle) > tolerance) { + *result_listener << "actual: gate[" << i + << "] angle=" << ops.at(i)->getParameter().front() + << ", expected: angle=" << angle + << ", tolerance=" << tolerance; + return false; + } + } + return true; +} +} // namespace testing + +// NOLINTEND(modernize-use-trailing-return-type) diff --git a/test/na/zoned/test_compiler.cpp b/test/na/zoned/test_compiler.cpp index 1dad1696e..f6288be3f 100644 --- a/test/na/zoned/test_compiler.cpp +++ b/test/na/zoned/test_compiler.cpp @@ -123,6 +123,33 @@ constexpr std::string_view fastRelaxedRoutingAwareConfiguration = R"({ } } })"; +constexpr std::string_view routingAwareNativeGateConfiguration = R"({ + "logLevel" : 1, + "codeGeneratorConfig" : { + "warnUnsupportedGates" : false + }, + "decomposerConfig" : { + "thetaOptSchedule" : true, + "checkFinalCond" : 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( \ @@ -185,6 +212,11 @@ COMPILER_TEST(RelaxedRoutingAwareCompiler, RoutingAwareCompiler, relaxedRoutingAwareConfiguration); COMPILER_TEST(FastRelaxedRoutingAwareCompiler, RoutingAwareCompiler, fastRelaxedRoutingAwareConfiguration); +COMPILER_TEST(FastRelaxedRoutingAwareNativeGateCompiler, + RoutingAwareNativeGateCompiler, + fastRelaxedRoutingAwareConfiguration); +COMPILER_TEST(RoutingAwareNativeGateCompiler, RoutingAwareNativeGateCompiler, + routingAwareNativeGateConfiguration); // 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_heuristic_placer.cpp b/test/na/zoned/test_heuristic_placer.cpp index 2b806b9f0..055078bb0 100644 --- a/test/na/zoned/test_heuristic_placer.cpp +++ b/test/na/zoned/test_heuristic_placer.cpp @@ -51,25 +51,25 @@ constexpr std::string_view configJson = R"({ "lookaheadFactor": 0.2, "reuseLevel": 5.0 })"; -class AStarPlacerPlaceTest : public ::testing::Test { +class AStarPlacerTest : public ::testing::Test { protected: Architecture architecture; HeuristicPlacer::Config config; HeuristicPlacer placer; - AStarPlacerPlaceTest() + AStarPlacerTest() : architecture(Architecture::fromJSONString(architectureJson)), config(nlohmann::json::parse(configJson) .template get()), placer(architecture, config) {} }; -TEST_F(AStarPlacerPlaceTest, Empty) { +TEST_F(AStarPlacerTest, Empty) { constexpr size_t nQubits = 1; EXPECT_THAT(placer.place(nQubits, std::vector>>{}, std::vector>{}), ::testing::ElementsAre(::testing::SizeIs(nQubits))); } -TEST_F(AStarPlacerPlaceTest, OneGate) { +TEST_F(AStarPlacerTest, OneGate) { constexpr size_t nQubits = 2; EXPECT_THAT(placer.place(nQubits, std::vector>>{ @@ -79,7 +79,7 @@ TEST_F(AStarPlacerPlaceTest, OneGate) { ::testing::SizeIs(nQubits), ::testing::SizeIs(nQubits))); } -TEST_F(AStarPlacerPlaceTest, TwoGatesCons) { +TEST_F(AStarPlacerTest, TwoGatesCons) { constexpr size_t nQubits = 4; const auto& placement = placer.place( nQubits, @@ -118,7 +118,7 @@ TEST_F(AStarPlacerPlaceTest, TwoGatesCons) { EXPECT_THAT(qubitsInEntanglementAsc, ::testing::ElementsAre(0U, 1U, 2U, 3U)); EXPECT_THAT(qubitsInEntanglementYs, ::testing::UnorderedElementsAre(70UL)); } -TEST_F(AStarPlacerPlaceTest, OneGateCross) { +TEST_F(AStarPlacerTest, OneGateCross) { constexpr size_t nQubits = 2; const auto& placement = placer.place( nQubits, std::vector>>{{{1U, 0U}}}, @@ -138,7 +138,7 @@ TEST_F(AStarPlacerPlaceTest, OneGateCross) { } EXPECT_THAT(qubitsInEntanglementAsc, ::testing::ElementsAre(0U, 1U)); } -TEST_F(AStarPlacerPlaceTest, TwoGatesZip) { +TEST_F(AStarPlacerTest, TwoGatesZip) { constexpr size_t nQubits = 4; const auto& placement = placer.place( nQubits, @@ -164,7 +164,7 @@ TEST_F(AStarPlacerPlaceTest, TwoGatesZip) { ::testing::ElementsAre(1U, 3U, 0U, 2U))); EXPECT_THAT(qubitsInEntanglementYs, ::testing::UnorderedElementsAre(70UL)); } -TEST_F(AStarPlacerPlaceTest, FullEntanglementZone) { +TEST_F(AStarPlacerTest, FullEntanglementZone) { constexpr size_t nQubits = 32; const auto& placement = placer.place( nQubits, @@ -196,7 +196,7 @@ TEST_F(AStarPlacerPlaceTest, FullEntanglementZone) { } EXPECT_THAT(qubitsLocationsInEntanglement, ::testing::SizeIs(nQubits)); } -TEST_F(AStarPlacerPlaceTest, TwoTwoQubitLayerReuse) { +TEST_F(AStarPlacerTest, TwoTwoQubitLayerReuse) { constexpr size_t nQubits = 3; const auto& placement = placer.place(nQubits, diff --git a/test/na/zoned/test_independent_set_router.cpp b/test/na/zoned/test_independent_set_router.cpp index 8585ae5f1..a7ca32191 100644 --- a/test/na/zoned/test_independent_set_router.cpp +++ b/test/na/zoned/test_independent_set_router.cpp @@ -41,24 +41,24 @@ constexpr std::string_view architectureJson = R"({ "aods":[{"id": 0, "site_separation": 2, "r": 20, "c": 20}], "rydberg_range": [[[5, 70], [55, 110]]] })"; -class IndependentSetRouterRouteTest : public ::testing::Test { +class IndependentSetRouterTest : public ::testing::Test { protected: Architecture architecture; IndependentSetRouter::Config config{ .method = IndependentSetRouter::Config::Method::STRICT}; IndependentSetRouter router; - IndependentSetRouterRouteTest() + IndependentSetRouterTest() : architecture(Architecture::fromJSONString(architectureJson)), router(architecture, config) {} }; -TEST_F(IndependentSetRouterRouteTest, Empty) { +TEST_F(IndependentSetRouterTest, Empty) { EXPECT_THAT( router.route( std::vector, size_t, size_t>>>{}), ::testing::IsEmpty()); } -TEST_F(IndependentSetRouterRouteTest, Initial) { +TEST_F(IndependentSetRouterTest, Initial) { const auto& slm = *architecture.storageZones.front(); EXPECT_THAT( router.route( @@ -67,7 +67,7 @@ TEST_F(IndependentSetRouterRouteTest, Initial) { {{slm, 0, 0}}}), ::testing::IsEmpty()); } -TEST_F(IndependentSetRouterRouteTest, OneLayer) { +TEST_F(IndependentSetRouterTest, OneLayer) { // STORAGE ... │ ... │ ... // 18 o o o o ... │ o o o o ... │ o o o o ... // 19 0 1 o o ... │ o o o o ... │ 0 1 o o ... @@ -93,7 +93,7 @@ TEST_F(IndependentSetRouterRouteTest, OneLayer) { ::testing::UnorderedElementsAre( ::testing::UnorderedElementsAre(0U, 1U)))); } -TEST_F(IndependentSetRouterRouteTest, Cross) { +TEST_F(IndependentSetRouterTest, Cross) { // STORAGE ... │ ... // 18 o o o o ... │ o o o o ... // 19 0 1 o o ... │ o o o o ... @@ -117,7 +117,7 @@ TEST_F(IndependentSetRouterRouteTest, Cross) { ::testing::UnorderedElementsAre(0U), ::testing::UnorderedElementsAre(1U)))); } -TEST_F(IndependentSetRouterRouteTest, Overtake) { +TEST_F(IndependentSetRouterTest, Overtake) { // STORAGE ... │ ... // 18 0 1 o o ... │ o o o o ... // 19 2 3 o o ... │ o o o o ... @@ -147,7 +147,7 @@ TEST_F(IndependentSetRouterRouteTest, Overtake) { ::testing::UnorderedElementsAre(0U, 1U), ::testing::UnorderedElementsAre(2U, 3U)))); } -TEST_F(IndependentSetRouterRouteTest, Array) { +TEST_F(IndependentSetRouterTest, Array) { // STORAGE ... │ ... // 18 0 1 2 3 o o ... │ o o o o o o ... // 19 4 5 6 7 o o ... │ o o o o o o ... diff --git a/test/na/zoned/test_native_gate_decomposer.cpp b/test/na/zoned/test_native_gate_decomposer.cpp new file mode 100644 index 000000000..293514523 --- /dev/null +++ b/test/na/zoned/test_native_gate_decomposer.cpp @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ir/QuantumComputation.hpp" +#include "na/zoned/decomposer/NativeGateDecomposer.hpp" +#include "na/zoned/matcher.hpp" +#include "na/zoned/scheduler/ASAPScheduler.hpp" + +#include +#include + +namespace na::zoned { +constexpr std::string_view architectureJson = R"({ + "name": "asap_scheduler_architecture", + "storage_zones": [{ + "zone_id": 0, + "slms": [{"id": 0, "site_separation": [3, 3], "r": 20, "c": 20, "location": [0, 0]}], + "offset": [0, 0], + "dimension": [60, 60] + }], + "entanglement_zones": [{ + "zone_id": 0, + "slms": [ + {"id": 1, "site_separation": [12, 10], "r": 4, "c": 4, "location": [5, 70]}, + {"id": 2, "site_separation": [12, 10], "r": 4, "c": 4, "location": [7, 70]} + ], + "offset": [5, 70], + "dimension": [50, 40] + }], + "aods":[{"id": 0, "site_separation": 2, "r": 20, "c": 20}], + "rydberg_range": [[[5, 70], [55, 110]]] +})"; + +class NativeGateDecomposerTest : public ::testing::Test { +protected: + Architecture architecture; + ASAPScheduler::Config schedulerConfig{.maxFillingFactor = .8}; + ASAPScheduler scheduler; + NativeGateDecomposer::Config decomposerConfig{}; + NativeGateDecomposer decomposer; + NativeGateDecomposerTest() + : architecture(Architecture::fromJSONString(architectureJson)), + scheduler(architecture, schedulerConfig), + decomposer(architecture, decomposerConfig) {} +}; + +TEST_F(NativeGateDecomposerTest, TranslationZ) { + const qc::StandardOperation op(0, qc::Z); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear(NativeGateDecomposer::Quaternion{0, 0, 0, 1}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationRZ) { + const qc::StandardOperation op(0, qc::RZ, {qc::PI_2}); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{1 / std::sqrt(2), 0, 0, + 1 / std::sqrt(2)}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationP) { + const qc::StandardOperation op(0, qc::P, {qc::PI_2}); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{1 / std::sqrt(2), 0, 0, + 1 / std::sqrt(2)}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationS) { + const qc::StandardOperation op(0, qc::S); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{1 / std::sqrt(2), 0, 0, + 1 / std::sqrt(2)}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationSdg) { + const qc::StandardOperation op(0, qc::Sdg); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{1 / std::sqrt(2), 0, 0, + -1 / std::sqrt(2)}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationT) { + const qc::StandardOperation op(0, qc::T); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{std::sqrt(2 + std::sqrt(2)) / 2, 0, + 0, std::sqrt(2 - std::sqrt(2)) / 2}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationTdg) { + const qc::StandardOperation op(0, qc::Tdg); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{std::sqrt(2 + std::sqrt(2)) / 2, 0, + 0, -std::sqrt(2 - std::sqrt(2)) / 2}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationX) { + const qc::StandardOperation op(0, qc::X); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear(NativeGateDecomposer::Quaternion{0, 1, 0, 0}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationRX) { + const qc::StandardOperation op(0, qc::RX, {qc::PI_2}); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{1 / std::sqrt(2), + 1 / std::sqrt(2), 0, 0}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationY) { + const qc::StandardOperation op(0, qc::Y); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear(NativeGateDecomposer::Quaternion{0, 0, 1, 0}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationRY) { + const qc::StandardOperation op(0, qc::RY, {qc::PI_2}); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{1 / std::sqrt(2), 0, + 1 / std::sqrt(2), 0}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationSX) { + const qc::StandardOperation op(0, qc::SX); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{1 / std::sqrt(2), + 1 / std::sqrt(2), 0, 0}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationSXdg) { + const qc::StandardOperation op(0, qc::SXdg); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{1 / std::sqrt(2), + -1 / std::sqrt(2), 0, 0}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TranslationU) { + qc::fp p = qc::PI_2; + qc::fp t = qc::PI_4; + qc::fp l = qc::PI_4; + const qc::StandardOperation op1(0, qc::U, {t, p, l}); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op1), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{ + std::cos(p / 2) * std::cos(t / 2) * std::cos(l / 2) - + std::sin(p / 2) * std::cos(t / 2) * std::sin(l / 2), + std::cos(p / 2) * std::sin(t / 2) * std::sin(l / 2) - + std::sin(p / 2) * std::cos(l / 2) * std::sin(t / 2), + std::cos(p / 2) * std::sin(t / 2) * std::cos(l / 2) + + std::sin(p / 2) * std::sin(l / 2) * std::sin(t / 2), + std::cos(p / 2) * std::cos(t / 2) * std::sin(l / 2) + + std::sin(p / 2) * std::cos(l / 2) * std::cos(t / 2)}, + NativeGateDecomposer::epsilon)); + + t = qc::PI_2; + const qc::StandardOperation op2(0, qc::U2, {p, l}); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op2), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{ + std::cos(p / 2) * std::cos(t / 2) * std::cos(l / 2) - + std::sin(p / 2) * std::cos(t / 2) * std::sin(l / 2), + std::cos(p / 2) * std::sin(t / 2) * std::sin(l / 2) - + std::sin(p / 2) * std::cos(l / 2) * std::sin(t / 2), + std::cos(p / 2) * std::sin(t / 2) * std::cos(l / 2) + + std::sin(p / 2) * std::sin(l / 2) * std::sin(t / 2), + std::cos(p / 2) * std::cos(t / 2) * std::sin(l / 2) + + std::sin(p / 2) * std::cos(l / 2) * std::cos(t / 2)}, + NativeGateDecomposer::epsilon)); + + t = -qc::PI_2; + l = -qc::PI_2; + + const qc::StandardOperation op3(0, qc::Vdg); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op3)), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{ + std::cos(p / 2) * std::cos(t / 2) * std::cos(l / 2) - + std::sin(p / 2) * std::cos(t / 2) * std::sin(l / 2), + std::cos(p / 2) * std::sin(t / 2) * std::sin(l / 2) - + std::sin(p / 2) * std::cos(l / 2) * std::sin(t / 2), + std::cos(p / 2) * std::sin(t / 2) * std::cos(l / 2) + + std::sin(p / 2) * std::sin(l / 2) * std::sin(t / 2), + std::cos(p / 2) * std::cos(t / 2) * std::sin(l / 2) + + std::sin(p / 2) * std::cos(l / 2) * std::cos(t / 2)}, + NativeGateDecomposer::epsilon)); + const qc::StandardOperation op4(0, qc::H); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion(op4), + ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{0, 1 / std::sqrt(2), 0, + 1 / std::sqrt(2)}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, CombineQuaternion1) { + const NativeGateDecomposer::Quaternion q1{cos(qc::PI_4), 0, 0, sin(qc::PI_4)}; + const NativeGateDecomposer::Quaternion q2{cos(qc::PI_2), 0, sin(qc::PI_2), 0}; + const auto& q12 = NativeGateDecomposer::combineQuaternions(q1, q2); + EXPECT_THAT(q12, ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{0, -cos(qc::PI_4), + cos(qc::PI_4), 0}, + NativeGateDecomposer::epsilon)); + const NativeGateDecomposer::Quaternion q3{cos(qc::PI_2), 0, 0, sin(qc::PI_2)}; + const auto& q13 = NativeGateDecomposer::combineQuaternions(q12, q3); + EXPECT_THAT(q13, ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{0, cos(qc::PI_4), + cos(qc::PI_4), 0}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, CombineQuaternion2) { + const NativeGateDecomposer::Quaternion q1{cos(qc::PI_2), 0, 0, sin(qc::PI_2)}; + const NativeGateDecomposer::Quaternion q2{cos(qc::PI_4 / 2), 0, + sin(qc::PI_4 / 2), 0}; + const auto& q12 = NativeGateDecomposer::combineQuaternions(q1, q2); + EXPECT_THAT(q12, ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{0, -sin(qc::PI_4 / 2), + 0, cos(qc::PI_4 / 2)}, + NativeGateDecomposer::epsilon)); + const NativeGateDecomposer::Quaternion q3{cos(qc::PI_4), 0, 0, sin(qc::PI_4)}; + const auto& q123 = NativeGateDecomposer::combineQuaternions(q12, q3); + const auto r = 1 / std::sqrt(2); + EXPECT_THAT(q123, ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{ + -r * cos(qc::PI_4 / 2), -r * sin(qc::PI_4 / 2), + r * sin(qc::PI_4 / 2), r * cos(qc::PI_4 / 2)}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, SingleXGateAngle) { + const qc::StandardOperation op(0, qc::X); + const auto& q = NativeGateDecomposer::convertGateToQuaternion(op); + EXPECT_THAT( + NativeGateDecomposer::getU3AnglesFromQuaternion(q), + ::testing::AnglesNear(NativeGateDecomposer::Angles{qc::PI, 0, qc::PI}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, SingleU3GateAngle) { + const qc::StandardOperation op(0, qc::U, {qc::PI_4, qc::PI, qc::PI_2}); + const auto& q = NativeGateDecomposer::convertGateToQuaternion(op); + const auto r = 1 / sqrt(2); + EXPECT_THAT(q, ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{ + -r * cos(qc::PI_4 / 2), -r * sin(qc::PI_4 / 2), + r * sin(qc::PI_4 / 2), r * cos(qc::PI_4 / 2)}, + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(NativeGateDecomposer::getU3AnglesFromQuaternion(q), + ::testing::AnglesNear( + NativeGateDecomposer::Angles{qc::PI_4, qc::PI, qc::PI_2}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, ThetaPiAngle) { + qc::StandardOperation op(0, qc::U, {qc::PI, qc::PI, qc::PI_2}); + const auto& q = NativeGateDecomposer::convertGateToQuaternion(op); + const auto r = 1 / sqrt(2); + EXPECT_THAT(q, ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{0, -r, r, 0}, + NativeGateDecomposer::epsilon)); + EXPECT_THAT( + NativeGateDecomposer::getU3AnglesFromQuaternion(q), + ::testing::AnglesNear(NativeGateDecomposer::Angles{qc::PI, 0, -qc::PI_2}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, ThetaZeroAngle) { + const qc::StandardOperation op(0, qc::U, {0, qc::PI, qc::PI_2}); + const auto& q = NativeGateDecomposer::convertGateToQuaternion(op); + const auto r = 1 / sqrt(2); + EXPECT_THAT(q, ::testing::QuaternionNear( + NativeGateDecomposer::Quaternion{-r, 0, 0, r}, + NativeGateDecomposer::epsilon)); + + EXPECT_THAT( + NativeGateDecomposer::getU3AnglesFromQuaternion(q), + ::testing::AnglesNear(NativeGateDecomposer::Angles{0, 0, 3 * qc::PI_2}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, DecompositionU3) { + constexpr NativeGateDecomposer::Angles u3{qc::PI_4, qc::PI, qc::PI_2}; + EXPECT_THAT(NativeGateDecomposer::getDecompositionAngles(u3, qc::PI_4), + ::testing::AnglesNear( + NativeGateDecomposer::Angles{qc::PI, qc::PI, -qc::PI_2}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, DecompositionX) { + constexpr NativeGateDecomposer::Angles x{qc::PI, -qc::PI_2, qc::PI_2}; + EXPECT_THAT(NativeGateDecomposer::getDecompositionAngles(x, qc::PI), + ::testing::AnglesNear(NativeGateDecomposer::Angles{qc::PI, 0, 0}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, DecompositionZ) { + constexpr NativeGateDecomposer::Angles z{0, 0, qc::PI}; + EXPECT_THAT( + NativeGateDecomposer::getDecompositionAngles(z, qc::PI), + ::testing::AnglesNear(NativeGateDecomposer::Angles{0, qc::PI_2, qc::PI_2}, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, OneRXOneQubit) { + // ┌───────┐ + // q: ┤ Rx(π) ├ + // └───────┘ + qc::QuantumComputation qc(1); + qc.rx(qc::PI, 0); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& decompSingleQubitLayers = + decomposer.decompose(qc.getNqubits(), singleQubitLayers, twoQubitLayers) + .singleQubitLayers; + EXPECT_EQ(decompSingleQubitLayers.size(), 1); + EXPECT_EQ(decompSingleQubitLayers[0].size(), 5); + EXPECT_THAT(decompSingleQubitLayers[0][0], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); + EXPECT_TRUE(decompSingleQubitLayers[0][1]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][1]->isGlobal(1)); + EXPECT_THAT(decompSingleQubitLayers[0][2], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI, + NativeGateDecomposer::epsilon)); + EXPECT_TRUE(decompSingleQubitLayers[0][3]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][3]->isGlobal(1)); + EXPECT_THAT(decompSingleQubitLayers[0][4], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, OneU3OneQubit) { + // ┌─────────────┐ + // q: ┤ U3(0,π,π/2) ├ + // └─────────────┘ + qc::QuantumComputation qc(1); + qc.u(0.0, qc::PI, qc::PI_2, 0); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& decompSingleQubitLayers = + decomposer.decompose(qc.getNqubits(), singleQubitLayers, twoQubitLayers) + .singleQubitLayers; + EXPECT_EQ(decompSingleQubitLayers.size(), 1); + EXPECT_EQ(decompSingleQubitLayers[0].size(), 5); + + EXPECT_THAT(decompSingleQubitLayers[0][0], + ::testing::ExpectRotationGate(qc::RZ, 0, 0, + NativeGateDecomposer::epsilon)); + EXPECT_TRUE(decompSingleQubitLayers[0][1]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][1]->isGlobal(1)); + EXPECT_THAT(decompSingleQubitLayers[0][2], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI, + NativeGateDecomposer::epsilon)); + EXPECT_TRUE(decompSingleQubitLayers[0][3]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][3]->isGlobal(1)); + EXPECT_THAT(decompSingleQubitLayers[0][4], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TwoGatesOneQubit) { + // ┌───────┐ ┌───────┐ + // q: ┤ X ├──┤ Z ├ + // └───────┘ └───────┘ + qc::QuantumComputation qc(1); + qc.x(0); + qc.z(0); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& decompSingleQubitLayers = + decomposer.decompose(qc.getNqubits(), singleQubitLayers, twoQubitLayers) + .singleQubitLayers; + + EXPECT_EQ(decompSingleQubitLayers.size(), 1); + EXPECT_EQ(decompSingleQubitLayers[0].size(), 5); + EXPECT_THAT(decompSingleQubitLayers[0][0], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); + EXPECT_TRUE(decompSingleQubitLayers[0][1]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][1]->isGlobal(1)); + EXPECT_THAT(decompSingleQubitLayers[0][2], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI, + NativeGateDecomposer::epsilon)); + EXPECT_TRUE(decompSingleQubitLayers[0][3]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][3]->isGlobal(1)); + EXPECT_THAT(decompSingleQubitLayers[0][4], + ::testing::ExpectRotationGate(qc::RZ, 0, 3 * qc::PI_2, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TwoGatesTwoQubits) { + // ┌───────┐ + // q_0: ─┤ X ├─ + // └───────┘ + // ┌───────┐ + // q_1: ─┤ Z ├─ + // └───────┘ + qc::QuantumComputation qc(2); + qc.x(0); + qc.z(1); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& decompSingleQubitLayers = + decomposer.decompose(qc.getNqubits(), singleQubitLayers, twoQubitLayers) + .singleQubitLayers; + EXPECT_EQ(decompSingleQubitLayers.size(), 1); + EXPECT_EQ(decompSingleQubitLayers[0].size(), 8); + + EXPECT_THAT(decompSingleQubitLayers[0][0], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(decompSingleQubitLayers[0][1], + ::testing::ExpectRotationGate(qc::RZ, 1, qc::PI_2, + NativeGateDecomposer::epsilon)); + + EXPECT_TRUE(decompSingleQubitLayers[0][2]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][2]->isGlobal(2)); + + EXPECT_THAT(decompSingleQubitLayers[0][3], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI, + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(decompSingleQubitLayers[0][4], + ::testing::ExpectRotationGate(qc::RZ, 1, 0, + NativeGateDecomposer::epsilon)); + + EXPECT_TRUE(decompSingleQubitLayers[0][5]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][5]->isGlobal(2)); + + EXPECT_THAT(decompSingleQubitLayers[0][6], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(decompSingleQubitLayers[0][7], + ::testing::ExpectRotationGate(qc::RZ, 1, qc::PI_2, + NativeGateDecomposer::epsilon)); +} + +TEST_F(NativeGateDecomposerTest, TwoQubitsTwoLayers) { + // ┌───────┐ ┌───────┐ + // q_0: ─┤ X ├───■───┤ Z ├─ + // └───────┘ │ └───────┘ + // │ ┌───────┐ + // q_1: ─────────────■───┤ X ├─ + // └───────┘ + qc::QuantumComputation qc(2); + qc.x(0); + qc.cz(0, 1); + qc.z(0); + qc.x(1); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& decompSingleQubitLayers = + decomposer.decompose(qc.getNqubits(), singleQubitLayers, twoQubitLayers) + .singleQubitLayers; + EXPECT_EQ(decompSingleQubitLayers.size(), 2); + EXPECT_EQ(decompSingleQubitLayers[0].size(), 5); + EXPECT_EQ(decompSingleQubitLayers[1].size(), 8); + + // Layer 1 + EXPECT_THAT(decompSingleQubitLayers[0][0], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); + + EXPECT_TRUE(decompSingleQubitLayers[0][1]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][1]->isGlobal(2)); + + EXPECT_THAT(decompSingleQubitLayers[0][2], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI, + NativeGateDecomposer::epsilon)); + + EXPECT_TRUE(decompSingleQubitLayers[0][3]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[0][3]->isGlobal(2)); + + EXPECT_THAT(decompSingleQubitLayers[0][4], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); + + // Layer 2 + EXPECT_THAT(decompSingleQubitLayers[1][0], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(decompSingleQubitLayers[1][1], + ::testing::ExpectRotationGate(qc::RZ, 1, qc::PI_2, + NativeGateDecomposer::epsilon)); + + EXPECT_TRUE(decompSingleQubitLayers[1][2]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[1][2]->isGlobal(2)); + + EXPECT_THAT(decompSingleQubitLayers[1][3], + ::testing::ExpectRotationGate(qc::RZ, 0, 0, + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(decompSingleQubitLayers[1][4], + ::testing::ExpectRotationGate(qc::RZ, 1, qc::PI, + NativeGateDecomposer::epsilon)); + + EXPECT_TRUE(decompSingleQubitLayers[1][5]->isCompoundOperation()); + EXPECT_TRUE(decompSingleQubitLayers[1][5]->isGlobal(2)); + + EXPECT_THAT(decompSingleQubitLayers[1][6], + ::testing::ExpectRotationGate(qc::RZ, 0, qc::PI_2, + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(decompSingleQubitLayers[1][7], + ::testing::ExpectRotationGate(qc::RZ, 1, qc::PI_2, + NativeGateDecomposer::epsilon)); +} + +} // namespace na::zoned diff --git a/test/na/zoned/test_theta_opt_scheduler.cpp b/test/na/zoned/test_theta_opt_scheduler.cpp new file mode 100644 index 000000000..8ae74cf11 --- /dev/null +++ b/test/na/zoned/test_theta_opt_scheduler.cpp @@ -0,0 +1,658 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ir/QuantumComputation.hpp" +#include "na/zoned/decomposer/NativeGateDecomposer.hpp" +#include "na/zoned/matcher.hpp" +#include "na/zoned/scheduler/ASAPScheduler.hpp" + +#include +#include +#include +#include +#include +#include + +namespace na::zoned { +constexpr std::string_view architectureJson = R"({ + "name": "asap_scheduler_architecture", + "storage_zones": [{ + "zone_id": 0, + "slms": [{"id": 0, "site_separation": [3, 3], "r": 20, "c": 20, "location": [0, 0]}], + "offset": [0, 0], + "dimension": [60, 60] + }], + "entanglement_zones": [{ + "zone_id": 0, + "slms": [ + {"id": 1, "site_separation": [12, 10], "r": 4, "c": 4, "location": [5, 70]}, + {"id": 2, "site_separation": [12, 10], "r": 4, "c": 4, "location": [7, 70]} + ], + "offset": [5, 70], + "dimension": [50, 40] + }], + "aods":[{"id": 0, "site_separation": 2, "r": 20, "c": 20}], + "rydberg_range": [[[5, 70], [55, 110]]] +})"; + +class ThetaOptTest : public ::testing::Test { +protected: + Architecture architecture; + ASAPScheduler::Config schedulerConfig{.maxFillingFactor = .8}; + ASAPScheduler scheduler; + NativeGateDecomposer::Config decomposerConfig{.thetaOptSchedule = true, + .checkFinalCond = false}; + NativeGateDecomposer decomposer; + ThetaOptTest() + : architecture(Architecture::fromJSONString(architectureJson)), + scheduler(architecture, schedulerConfig), + decomposer(architecture, decomposerConfig) {} +}; + +TEST_F(ThetaOptTest, Graph) { + // Circuit + // ┌───────┐ ┌───────┐ + // q_0: ─┤ X ├───■───┤ Z ├─ + // └───────┘ │ └───────┘ + // │ ┌───────┐ + // q_1: ─────────────■───┤ X ├─ + // └───────┘ + qc::QuantumComputation qc(2); + qc.x(0); + qc.cz(0, 1); + qc.z(0); + qc.x(1); + + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& u3Layers = + NativeGateDecomposer::transformToU3(singleQubitLayers, 2); + const auto& dag = + NativeGateDecomposer::convertCircuitToDAG({u3Layers, twoQubitLayers}, 2); + EXPECT_EQ(std::get(dag.getNodeValue(0)).qubit, + 0); + EXPECT_THAT( + std::get(dag.getNodeValue(0)).angles, + ::testing::AnglesNear(u3Layers.front().front().angles, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(dag.getAdjacent(0), + ::testing::ElementsAre(::testing::Pair( + ::testing::Eq(1), + ::testing::DoubleNear(1.0, NativeGateDecomposer::epsilon)))); + + const auto& tqg = std::get>(dag.getNodeValue(1)); + EXPECT_THAT(tqg, ::testing::ElementsAre(0UL, 1UL)); + EXPECT_THAT(dag.getAdjacent(1), + ::testing::ElementsAre( + ::testing::Pair(::testing::Eq(2), + ::testing::DoubleNear( + 1.0, NativeGateDecomposer::epsilon)), + ::testing::Pair(::testing::Eq(3), + ::testing::DoubleNear( + 1.0, NativeGateDecomposer::epsilon)))); + + EXPECT_EQ(std::get(dag.getNodeValue(2)).qubit, + 0); + EXPECT_THAT( + std::get(dag.getNodeValue(2)).angles, + ::testing::AnglesNear(u3Layers.at(1).front().angles, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(dag.getAdjacent(2), ::testing::IsEmpty()); + + EXPECT_EQ(std::get(dag.getNodeValue(3)).qubit, + 1); + EXPECT_THAT( + std::get(dag.getNodeValue(3)).angles, + ::testing::AnglesNear(u3Layers.at(1).at(1).angles, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(dag.getAdjacent(3), ::testing::IsEmpty()); +} + +TEST_F(ThetaOptTest, Sift) { + // Circuit + // ┌───────┐ ┌───────┐ ┌───────┐ + // q_0: ──┤ X ├───■───────┤ Z ├───■───┤ Y ├─ + // └───────┘ │ └───────┘ │ └───────┘ + // │ ┌───────┐ │ + // q_1: ──────────────■───■───┤ X ├───│───────────── + // │ └───────┘ │ + // ┌───────┐ │ ┌───────┐ │ + // q_1: ──┤ X ├───────■───┤ Y ├───■───────────── + // └───────┘ └───────┘ + qc::QuantumComputation qc(3); + qc.x(0); + qc.x(2); + qc.cz(0, 1); + qc.cz(1, 2); + qc.z(0); + qc.x(1); + qc.y(2); + qc.cz(0, 2); + qc.y(0); + + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& u3Layers = + NativeGateDecomposer::transformToU3(singleQubitLayers, 3); + const auto& graph = + NativeGateDecomposer::convertCircuitToDAG({u3Layers, twoQubitLayers}, 3); + + const auto& subproblem1 = + NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6, 7, 8}, 3); + + EXPECT_THAT( + subproblem1, + ::testing::ElementsAre(::testing::IsEmpty(), ::testing::ElementsAre(0, 1), + ::testing::ElementsAre(2, 3, 4, 5, 6, 7, 8))); + + const auto& subproblem2 = + NativeGateDecomposer::sift(graph, subproblem1[2], 3); + + EXPECT_THAT(subproblem2, + ::testing::ElementsAre(::testing::ElementsAre(2, 4), + ::testing::ElementsAre(3, 5, 6), + ::testing::ElementsAre(7, 8))); + + const auto& subproblem3 = + NativeGateDecomposer::sift(graph, subproblem2[2], 3); + + EXPECT_THAT(subproblem3, ::testing::ElementsAre(::testing::ElementsAre(7), + ::testing::ElementsAre(8), + ::testing::IsEmpty())); +} + +TEST_F(ThetaOptTest, NextLayersPush) { + // Circuit + // ┌─────────────────┐ ┌───────┐ + // q_0: ──┤ U(PI,Pi/2,PI/4) ├─────────■───┤ X ├─────────■──── + // └─────────────────┘ │ └───────┘ │ + // │ ┌───────┐ │ + // q_1: ──────────────────────────■───■───┤ Y ├─────────│──── + // │ └───────┘ │ + // ┌───────────────────┐ │ ┌────────────────┐ │ + // q_2: ──┤ U(PI/4,PI/4,PI/4) ├───■───┤ U(PI/2,0,PI/2) ├────■──── + // └───────────────────┘ └────────────────┘ + qc::QuantumComputation qc(3); + qc.u(qc::PI, qc::PI_2, qc::PI_4, 0); + qc.u(qc::PI_4, qc::PI_4, qc::PI_4, 2); + qc.cz(1, 2); + qc.cz(0, 1); + qc.x(0); + qc.y(1); + qc.u(qc::PI_2, 0.0, qc::PI_2, 2); + qc.cz(0, 2); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& u3Layers = + NativeGateDecomposer::transformToU3(singleQubitLayers, 3); + const auto& graph = + NativeGateDecomposer::convertCircuitToDAG({u3Layers, twoQubitLayers}, 3); + const auto& subproblem1 = + NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6, 7}, 3); + const auto& subproblem2 = + NativeGateDecomposer::sift(graph, subproblem1[2], 3); + const auto& layers = NativeGateDecomposer::getPossibleLayers( + graph, subproblem1[1], subproblem2, false); + + EXPECT_THAT( + layers, + ::testing::ElementsAre( + ::testing::Pair( + ::testing::ElementsAre(::testing::UnorderedElementsAre(0, 1), + ::testing::UnorderedElementsAre(2, 4), + ::testing::UnorderedElementsAre(3, 5, 6), + ::testing::UnorderedElementsAre(7)), + ::testing::DoubleNear(qc::PI, NativeGateDecomposer::epsilon)), + ::testing::Pair( + ::testing::ElementsAre( + ::testing::UnorderedElementsAre(1), + ::testing::UnorderedElementsAre(2), + ::testing::UnorderedElementsAre(3, 0), + ::testing::UnorderedElementsAre(4, 5, 6, 7)), + ::testing::DoubleNear(qc::PI_4, NativeGateDecomposer::epsilon)))); +} + +TEST_F(ThetaOptTest, NextLayersCond2) { + // Circuit + // ┌─────────────────┐ ┌───────┐ + // q_0: ──┤ U(PI,Pi/2,PI/4) ├─────■───■───┤ X ├─────────■──── + // └─────────────────┘ │ │ └───────┘ │ + // │ │ ┌───────┐ │ + // q_1: ──────────────────────────│───■───┤ Y ├─────────│──── + // │ └───────┘ │ + // ┌───────────────────┐ │ ┌────────────────┐ │ + // q_2: ──┤ U(PI/4,PI/4,PI/4) ├───■───┤ U(PI/2,0,PI/2) ├────■──── + // └───────────────────┘ └────────────────┘ + qc::QuantumComputation qc(3); + qc.u(qc::PI, qc::PI_2, qc::PI_4, 0); + qc.u(qc::PI_4, qc::PI_4, qc::PI_4, 2); + qc.cz(0, 2); + qc.cz(0, 1); + qc.x(0); + qc.y(1); + qc.u(qc::PI_2, 0.0, qc::PI_2, 2); + qc.cz(0, 2); + + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& u3Layers = + NativeGateDecomposer::transformToU3(singleQubitLayers, 3); + const auto& graph = + NativeGateDecomposer::convertCircuitToDAG({u3Layers, twoQubitLayers}, 3); + const auto& subproblem1 = + NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6, 7}, 3); + const auto& subproblem = NativeGateDecomposer::sift(graph, subproblem1[2], 3); + const auto& layers = NativeGateDecomposer::getPossibleLayers( + graph, subproblem1[1], subproblem, false); + + EXPECT_EQ(layers.size(), 1); + + EXPECT_THAT(layers[0].second, + ::testing::DoubleNear(qc::PI, NativeGateDecomposer::epsilon)); + EXPECT_THAT(layers[0].first[0], ::testing::UnorderedElementsAre(0, 1)); + EXPECT_THAT(layers[0].first[1], ::testing::UnorderedElementsAre(2, 4)); + EXPECT_THAT(layers[0].first[2], ::testing::UnorderedElementsAre(3, 5, 6)); + EXPECT_THAT(layers[0].first[3], ::testing::UnorderedElementsAre(7)); +} + +TEST_F(ThetaOptTest, NextLayersCond3) { + // Circuit + // ┌──────────────────┐ ┌───────┐ + // q_0: ──┤ U(-PI,Pi/2,PI/4) ├─────────■───┤ X ├─────■──── + // └──────────────────┘ │ └───────┘ │ + // │ ┌───────┐ │ + // q_1: ──────────────────────────■────■───┤ Y ├─────│──── + // │ └───────┘ │ + // ┌───────────────────┐ │ │ + // q_2: ──┤ U(PI/4,PI/4,PI/4) ├───■──────────────────────■──── + // └───────────────────┘ + qc::QuantumComputation qc(3); + qc.u(-qc::PI, qc::PI_2, qc::PI_4, 0); + qc.u(qc::PI_4, qc::PI_4, qc::PI_4, 2); + qc.cz(1, 2); + qc.cz(0, 1); + qc.x(0); + qc.y(1); + qc.cz(0, 2); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& u3Layers = + NativeGateDecomposer::transformToU3(singleQubitLayers, 3); + const auto& graph = + NativeGateDecomposer::convertCircuitToDAG({u3Layers, twoQubitLayers}, 3); + const auto& subproblem = + NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6}, 3); + const auto& subproblem2 = NativeGateDecomposer::sift(graph, subproblem[2], 3); + const auto& Layers = NativeGateDecomposer::getPossibleLayers( + graph, subproblem[1], subproblem2, false); + + EXPECT_EQ(Layers.size(), 1); + + EXPECT_THAT(Layers[0].second, + ::testing::DoubleNear(qc::PI, NativeGateDecomposer::epsilon)); + EXPECT_THAT(Layers[0].first[0], ::testing::UnorderedElementsAre(0, 1)); + EXPECT_THAT(Layers[0].first[1], ::testing::UnorderedElementsAre(2, 3)); + EXPECT_THAT(Layers[0].first[2], ::testing::UnorderedElementsAre(4, 5)); + EXPECT_THAT(Layers[0].first[3], ::testing::UnorderedElementsAre(6)); +} + +TEST_F(ThetaOptTest, RecursionBase) { + // Circuit + // ┌─────────────────┐ ┌───────┐ + // q_0: ──┤ U(PI,Pi/2,PI/4) ├─────────■───┤ X ├─────────■─ ─ ─ + // └─────────────────┘ │ └───────┘ │ + // │ ┌───────┐ │ + // q_1: ──────────────────────────■───■───┤ Y ├─────────│─ ─ ─ + // │ └───────┘ │ + // ┌───────────────────┐ │ ┌────────────────┐ │ + // q_2: ──┤ U(PI/4,PI/4,PI/4) ├───■───┤ U(PI/2,0,PI/2) ├────■─ ─ ─ + // └───────────────────┘ └────────────────┘ + // + // ┌─────────────────┐ + // q_0: ─ ─ ─┤ U(PI/2,PI/2,PI) ├── + // └─────────────────┘ + // + // q_1: ─ ─ ────────────────────── + // + // q_2: ─ ─ ────────────────────── + + size_t n = 3; + qc::QuantumComputation qc(n); + qc.u(qc::PI, qc::PI_2, qc::PI_4, 0); + qc.u(qc::PI_4, qc::PI_4, qc::PI_4, 2); + qc.cz(1, 2); + qc.cz(0, 1); + qc.x(0); + qc.y(1); + qc.u(qc::PI_2, 0.0, qc::PI_2, 2); + qc.cz(0, 2); + qc.y(0); + qc.u(qc::PI_2, qc::PI_2, qc::PI, 0); + + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& u3Layers = + NativeGateDecomposer::transformToU3(singleQubitLayers, n); + const auto& graph = + NativeGateDecomposer::convertCircuitToDAG({u3Layers, twoQubitLayers}, n); + const auto& subproblem = + NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6, 7, 8}, n); + NativeGateDecomposer::DirectedGraph< + std::pair, std::vector>> + subproblemGraph; + subproblemGraph.addNode({}); + std::unordered_map, 3>, + std::pair>, SubproblemHasher> + memo; + const auto& result = NativeGateDecomposer::scheduleRemaining( + subproblem, graph, subproblemGraph, 0, n, false, memo); + + EXPECT_EQ(result, 5 * qc::PI_2); + + EXPECT_EQ(subproblemGraph.size(), 7); + EXPECT_THAT( + subproblemGraph.getAdjacent(0), + ::testing::UnorderedElementsAre( + ::testing::Pair( + ::testing::Eq(1), + ::testing::DoubleNear(qc::PI, NativeGateDecomposer::epsilon)), + ::testing::Pair( + ::testing::Eq(4), + ::testing::DoubleNear(qc::PI_4, NativeGateDecomposer::epsilon)))); + + EXPECT_THAT(subproblemGraph.getNodeValue(1), + ::testing::Pair(::testing::IsEmpty(), + ::testing::UnorderedElementsAre(0, 1))); + EXPECT_THAT( + subproblemGraph.getAdjacent(1), + ::testing::UnorderedElementsAre(::testing::Pair( + ::testing::Eq(2), + ::testing::DoubleNear(qc::PI, NativeGateDecomposer::epsilon)))); + EXPECT_THAT(subproblemGraph.getNodeValue(2), + ::testing::Pair(::testing::UnorderedElementsAre(2, 4), + ::testing::UnorderedElementsAre(3, 5, 6))); + EXPECT_THAT( + subproblemGraph.getAdjacent(2), + ::testing::UnorderedElementsAre(::testing::Pair( + ::testing::Eq(3), + ::testing::DoubleNear(qc::PI_2, NativeGateDecomposer::epsilon)))); + EXPECT_THAT(subproblemGraph.getNodeValue(3), + ::testing::Pair(::testing::UnorderedElementsAre(7), + ::testing::UnorderedElementsAre(8))); + EXPECT_THAT(subproblemGraph.getAdjacent(3), ::testing::IsEmpty()); + + EXPECT_THAT(subproblemGraph.getNodeValue(4), + ::testing::Pair(::testing::IsEmpty(), + ::testing::UnorderedElementsAre(1))); + EXPECT_THAT( + subproblemGraph.getAdjacent(4), + ::testing::UnorderedElementsAre(::testing::Pair( + ::testing::Eq(5), + ::testing::DoubleNear(qc::PI, NativeGateDecomposer::epsilon)))); + EXPECT_THAT(subproblemGraph.getNodeValue(5), + ::testing::Pair(::testing::UnorderedElementsAre(2), + ::testing::UnorderedElementsAre(0, 3))); + EXPECT_THAT( + subproblemGraph.getAdjacent(5), + ::testing::UnorderedElementsAre(::testing::Pair( + ::testing::Eq(6), + ::testing::DoubleNear(qc::PI, NativeGateDecomposer::epsilon)))); + EXPECT_THAT(subproblemGraph.getNodeValue(6), + ::testing::Pair(::testing::UnorderedElementsAre(4), + ::testing::UnorderedElementsAre(5, 6))); + EXPECT_THAT( + subproblemGraph.getAdjacent(6), + ::testing::UnorderedElementsAre(::testing::Pair( + ::testing::Eq(3), + ::testing::DoubleNear(qc::PI_2, NativeGateDecomposer::epsilon)))); +} + +TEST_F(ThetaOptTest, CheapestPath) { + NativeGateDecomposer::DirectedGraph< + std::pair, std::vector>> + subproblemGraph; + for (int i = 0; i < 14; i++) { + subproblemGraph.addNode({}); + } + subproblemGraph.addEdge(0, 1); + subproblemGraph.addEdge(0, 2); + subproblemGraph.addEdge(0, 3, 0.5); + subproblemGraph.addEdge(1, 4); + subproblemGraph.addEdge(2, 5); + subproblemGraph.addEdge(3, 6); + subproblemGraph.addEdge(3, 7); + subproblemGraph.addEdge(4, 8); + subproblemGraph.addEdge(5, 8); + subproblemGraph.addEdge(5, 9); + subproblemGraph.addEdge(6, 10); + subproblemGraph.addEdge(7, 11); + subproblemGraph.addEdge(8, 12); + subproblemGraph.addEdge(11, 13); + const auto& leafNodes = NativeGateDecomposer::findLeafNodes(subproblemGraph); + const auto& path = + NativeGateDecomposer::findCheapestPath(subproblemGraph, leafNodes); + EXPECT_THAT(leafNodes, ::testing::ElementsAre(9, 10, 12, 13)); + EXPECT_THAT(path, ::testing::ElementsAre(3, 6, 10)); +} + +TEST_F(ThetaOptTest, BuildSchedule) { + // Circuit + // ┌───────┐ ┌───────┐ ┌───────┐ + // q_0: ──┤ X ├───■───────┤ Z ├───■───┤ Y ├─ + // └───────┘ │ └───────┘ │ └───────┘ + // │ ┌───────┐ │ + // q_1: ──────────────■───■───┤ X ├───│───────────── + // │ └───────┘ │ + // ┌───────┐ │ ┌───────┐ │ + // q_2: ──┤ X ├───────■───┤ Y ├───■───────────── + // └───────┘ └───────┘ + qc::QuantumComputation qc(3); + qc.x(0); + qc.x(2); + qc.cz(0, 1); + qc.cz(1, 2); + qc.z(0); + qc.x(1); + qc.y(2); + qc.cz(0, 2); + qc.y(0); + + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& u3Layers = + NativeGateDecomposer::transformToU3(singleQubitLayers, 3); + const auto& graph = + NativeGateDecomposer::convertCircuitToDAG({u3Layers, twoQubitLayers}, 3); + + // Create a basic subproblem graph from a purely sifted schedule + NativeGateDecomposer::DirectedGraph< + std::pair, std::vector>> + subproblemGraph; + subproblemGraph.addNode({}); + subproblemGraph.addNode({{}, {0, 1}}); + subproblemGraph.addNode({{2, 4}, {3, 5, 6}}); + subproblemGraph.addNode({{7}, {8}}); + subproblemGraph.addEdge(0, 1, NativeGateDecomposer::maxTheta(graph, {0, 1})); + subproblemGraph.addEdge(1, 2, + NativeGateDecomposer::maxTheta(graph, {3, 5, 6})); + subproblemGraph.addEdge(2, 3, NativeGateDecomposer::maxTheta(graph, {8})); + + const auto& [singleQubitLayersDecomposed, twoQubitLayersDecomposed] = + NativeGateDecomposer::buildSchedule(graph, subproblemGraph); + + EXPECT_THAT(singleQubitLayersDecomposed, ::testing::SizeIs(4)); + EXPECT_THAT(twoQubitLayersDecomposed, ::testing::SizeIs(3)); + + EXPECT_THAT(singleQubitLayersDecomposed.at(0).at(0), + ::testing::U3GateNear(u3Layers.at(0).at(0), + NativeGateDecomposer::epsilon)); + EXPECT_THAT(singleQubitLayersDecomposed.at(0).at(1), + ::testing::U3GateNear(u3Layers.at(0).at(1), + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(twoQubitLayersDecomposed.at(0), + ::testing::ElementsAre(::testing::ElementsAre(0, 1))); + + EXPECT_THAT(singleQubitLayersDecomposed.at(1), ::testing::IsEmpty()); + + EXPECT_THAT(twoQubitLayersDecomposed.at(1), + ::testing::ElementsAre(::testing::ElementsAre(1, 2))); + + EXPECT_THAT(singleQubitLayersDecomposed.at(2).at(0), + ::testing::U3GateNear(u3Layers.at(1).at(0), + NativeGateDecomposer::epsilon)); + EXPECT_THAT(singleQubitLayersDecomposed.at(2).at(1), + ::testing::U3GateNear(u3Layers.at(2).at(0), + NativeGateDecomposer::epsilon)); + EXPECT_THAT(singleQubitLayersDecomposed.at(2).at(2), + ::testing::U3GateNear(u3Layers.at(2).at(1), + NativeGateDecomposer::epsilon)); + + EXPECT_THAT(twoQubitLayersDecomposed.at(2), + ::testing::ElementsAre(::testing::ElementsAre(0, 2))); + EXPECT_THAT(singleQubitLayersDecomposed.at(3).at(0), + ::testing::U3GateNear(u3Layers.at(3).at(0), + NativeGateDecomposer::epsilon)); +} + +TEST_F(ThetaOptTest, CompleteTestSmall) { + // Circuit + // ┌─────────────────┐ ┌───────┐ + // q_0: ──┤ U(PI,PI/2,PI/4) ├─────────■───┤ X ├─────────■─ ─ ─ + // └─────────────────┘ │ └───────┘ │ + // │ ┌───────┐ │ + // q_1: ──────────────────────────■───■───┤ Y ├─────────│─ ─ ─ + // │ └───────┘ │ + // ┌───────────────────┐ │ ┌────────────────┐ │ + // q_2: ──┤ U(PI/4,PI/4,PI/4) ├───■───┤ U(PI/2,0,PI/2) ├────■─ ─ ─ + // └───────────────────┘ └────────────────┘ + // + // ┌─────────────────┐ + // q_0: ─ ─ ─┤ U(PI/2,PI/2,PI) ├── + // └─────────────────┘ + // + // q_1: ─ ─ ────────────────────── + // + // q_2: ─ ─ ────────────────────── + + qc::QuantumComputation qc(3); + qc.u(qc::PI, qc::PI_2, qc::PI_4, 0); + qc.u(qc::PI_4, qc::PI_4, qc::PI_4, 2); + qc.cz(1, 2); + qc.cz(0, 1); + qc.x(0); + qc.y(1); + qc.u(qc::PI_2, 0.0, qc::PI_2, 2); + qc.cz(0, 2); + qc.y(0); + qc.u(qc::PI_2, qc::PI_2, qc::PI, 0); + + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& u3Layers = + NativeGateDecomposer::transformToU3(singleQubitLayers, 3); + const auto& [optU3Layers, optTwoQubitLayers] = + decomposer.scheduleThetaOpt({u3Layers, twoQubitLayers}, 3); + + EXPECT_THAT(optU3Layers, ::testing::SizeIs(4)); + EXPECT_THAT(optTwoQubitLayers, ::testing::SizeIs(3)); + + EXPECT_THAT(optU3Layers.at(0), ::testing::SizeIs(2)); + EXPECT_THAT(optU3Layers.at(1), ::testing::SizeIs(0)); + EXPECT_THAT(optU3Layers.at(2), ::testing::SizeIs(3)); + EXPECT_THAT(optU3Layers.at(3), ::testing::SizeIs(1)); + + EXPECT_THAT(optU3Layers.at(0).at(0), + ::testing::U3GateNear(u3Layers.at(0).at(0), + NativeGateDecomposer::epsilon)); + EXPECT_THAT(optU3Layers.at(0).at(1), + ::testing::U3GateNear(u3Layers.at(0).at(1), + NativeGateDecomposer::epsilon)); + EXPECT_THAT(optTwoQubitLayers.at(0), + ::testing::ElementsAre(::testing::ElementsAre(1, 2))); + EXPECT_THAT(optTwoQubitLayers.at(1), + ::testing::ElementsAre(::testing::ElementsAre(0, 1))); + EXPECT_THAT(optU3Layers.at(2).at(0), + ::testing::U3GateNear(u3Layers.at(1).at(0), + NativeGateDecomposer::epsilon)); + EXPECT_THAT(optU3Layers.at(2).at(1), + ::testing::U3GateNear(u3Layers.at(2).at(0), + NativeGateDecomposer::epsilon)); + EXPECT_THAT(optU3Layers.at(2).at(2), + ::testing::U3GateNear(u3Layers.at(2).at(1), + NativeGateDecomposer::epsilon)); + EXPECT_THAT(optTwoQubitLayers.at(2), + ::testing::ElementsAre(::testing::ElementsAre(0, 2))); + EXPECT_THAT(optU3Layers.at(3).at(0), + ::testing::U3GateNear(u3Layers.at(3).at(0), + NativeGateDecomposer::epsilon)); +} + +TEST_F(ThetaOptTest, Complete) { + qc::QuantumComputation qc(4); + qc.u(qc::PI, qc::PI_2, qc::PI_4, 0); + qc.u(qc::PI_4, qc::PI_2, qc::PI_2, 1); + qc.u(qc::PI_2, qc::PI_2, qc::PI_2, 2); + qc.cz(1, 2); + qc.cz(2, 3); + qc.cz(0, 1); + qc.u(qc::PI_2, qc::PI_4, qc::PI_2, 2); + qc.u(qc::PI_2, qc::PI_2, qc::PI_4, 3); + qc.cz(2, 3); + qc.u(qc::PI, qc::PI_2, qc::PI_4, 2); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& [decompSingleQubitLayers, decompTwoQubitLayers] = + decomposer.decompose(4, singleQubitLayers, twoQubitLayers); + + EXPECT_THAT(decompSingleQubitLayers, ::testing::SizeIs(5)); + EXPECT_THAT(decompSingleQubitLayers.at(0), ::testing::SizeIs(8)); + EXPECT_THAT(decompSingleQubitLayers.at(0).at(0), + ::testing::ExpectRotationGate(qc::RZ, 2, qc::PI_2, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(decompSingleQubitLayers.at(0).at(1), + ::testing::ExpectRotationGate(qc::RZ, 1, 2.7145140671973169, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(decompSingleQubitLayers.at(0).at(2), + ::testing::ExpectGlobalRotationGate( + qc::RY, 4U, qc::PI_4, NativeGateDecomposer::epsilon)); + EXPECT_THAT(decompSingleQubitLayers.at(0).at(3), + ::testing::ExpectRotationGate(qc::RZ, 2, qc::PI, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(decompSingleQubitLayers.at(0).at(4), + ::testing::ExpectRotationGate(qc::RZ, 1, 1.1437177404024206, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(decompSingleQubitLayers.at(0).at(5), + ::testing::ExpectGlobalRotationGate( + qc::RY, 4U, -qc::PI_4, NativeGateDecomposer::epsilon)); + EXPECT_THAT(decompSingleQubitLayers.at(0).at(6), + ::testing::ExpectRotationGate(qc::RZ, 2, -qc::PI_2, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(decompSingleQubitLayers.at(0).at(7), + ::testing::ExpectRotationGate(qc::RZ, 1, -0.4270785863924762, + NativeGateDecomposer::epsilon)); + EXPECT_THAT(decompSingleQubitLayers.at(1), ::testing::IsEmpty()); + EXPECT_THAT(decompSingleQubitLayers.at(2), ::testing::SizeIs(8)); + EXPECT_THAT(decompSingleQubitLayers.at(3), ::testing::SizeIs(8)); + EXPECT_THAT(decompSingleQubitLayers.at(4), ::testing::IsEmpty()); + + EXPECT_THAT(decompTwoQubitLayers, + ::testing::ElementsAre( + ::testing::ElementsAre(::testing::ElementsAre(1, 2)), + ::testing::ElementsAre(::testing::ElementsAre(2, 3)), + ::testing::ElementsAre(::testing::ElementsAre(2, 3)), + ::testing::ElementsAre(::testing::ElementsAre(0, 1)))); +} + +TEST_F(ThetaOptTest, SiftOrder) { + qc::QuantumComputation qc(2); + qc.cz(0, 1); + qc.x(0); + const auto& [singleQubitLayers, twoQubitLayers] = scheduler.schedule(qc); + const auto& [decomposedSingleQubitLayers, decomposedTwoQubitLayers] = + decomposer.decompose(2, singleQubitLayers, twoQubitLayers); + EXPECT_THAT(decomposedSingleQubitLayers, + ::testing::ElementsAre(::testing::IsEmpty(), + ::testing::Not(::testing::IsEmpty()))); + EXPECT_THAT(decomposedTwoQubitLayers, + ::testing::ElementsAre(::testing::Not(::testing::IsEmpty()))); +} +} // namespace na::zoned diff --git a/test/python/na/test_zoned.py b/test/python/na/test_zoned.py index 2018cfb44..5c1d0cf2d 100644 --- a/test/python/na/test_zoned.py +++ b/test/python/na/test_zoned.py @@ -11,6 +11,7 @@ from __future__ import annotations from pathlib import Path +from typing import cast import pytest from mqt.core import load @@ -67,4 +68,4 @@ def test_na_routing_aware_compiler(compiler: RoutingAwareCompiler, circuit_filen assert result is not None stats = compiler.stats() assert "totalTime" in stats - assert stats["totalTime"] > 0 + assert cast("float", stats["totalTime"]) > 0