diff --git a/bindings/na/register_zoned.cpp b/bindings/na/register_zoned.cpp index 272575cd2..5f9140ca0 100644 --- a/bindings/na/register_zoned.cpp +++ b/bindings/na/register_zoned.cpp @@ -371,6 +371,169 @@ void registerZoned(nb::module_& m) { }, 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)}; + }, + "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. + Returns: The statistics as a dictionary)pb"); } 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..093c178c2 --- /dev/null +++ b/eval/na/zoned/eval_native_gate_decomposition.py @@ -0,0 +1,705 @@ +#!/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==3.7.1", +# ] +# [tool.uv] +# exclude-newer = "2026-06-30T12: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 +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 mqt.qmap.na.zoned import ( + PlacementMethod, + RoutingAwareCompiler, + # RoutingAwareAxialCompiler, + RoutingAwareNativeGateCompiler, + 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 | 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, +) -> 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 = _compile_wrapper( + compiler, qc + ) # 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 _process_ry(self, line: str, it: Iterator[str]) -> None: # noqa: ARG002 + """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: + 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) + 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") + + +def main() -> None: + """Main function for evaluating the native gate compiler.""" + # set working directory to script location + os.chdir(pathlib.Path(pathlib.Path(__file__).resolve()).parent) + print("\033[32m[INFO]\033[0m Reading in architecture...") + with pathlib.Path("square_architecture.json").open(encoding="utf-8") as f: + arch_dict = json.load(f) + 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..ca0a62374 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 @@ -185,7 +186,7 @@ class Compiler : protected Scheduler, spdlog::should_log(spdlog::level::debug)) { const auto& [min, sum, max] = std::accumulate( twoQubitGateLayers.cbegin(), twoQubitGateLayers.cend(), - std::array{std::numeric_limits::max(), 0UL, 0UL}, + std::array{std::numeric_limits::max(), 0UL, 0UL}, [](const auto& acc, const auto& layer) -> std::array { const auto& [minAcc, sumAcc, maxAcc] = acc; const auto n = layer.size(); @@ -203,7 +204,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 - @@ -311,4 +313,17 @@ class RoutingAwareCompiler final explicit RoutingAwareCompiler(const Architecture& architecture) : Compiler(architecture) {} }; + +class RoutingAwareNativeGateCompiler 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..2a05a7ba0 --- /dev/null +++ b/include/na/zoned/decomposer/NativeGateDecomposer.hpp @@ -0,0 +1,461 @@ +/* + * 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 + +namespace na::zoned { + +/** + * 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 { + /** + * 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 value to use as a margin of error for float equality + constexpr static qc::fp epsilon = + std::numeric_limits::epsilon() * 1024; + + /** + * 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 minimal struct to store the parameters of a U3 gate along with the qubit + * it acts on. + */ + struct StructU3 { + Angles angles; + qc::Qubit qubit; + }; + +public: + /// 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 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; + + [[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 DiGraph { + /// number of nodes in the graph + size_t nodeNumber; + /// 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: + /** + * @brief Creates an empty graph to hold objects of type T + */ + DiGraph() { + nodeNumber = 0; + adjacencies_ = std::vector>>(); + nodeValues_ = std::vector(); + } + + /** + * @brief Adds a node with 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 add_Node(T node) -> size_t { + adjacencies_.emplace_back(); + nodeValues_.push_back(std::move(node)); + return nodeNumber++; + } + + /** + * @brief Adds an edge between two nodes to the graph with given weight + * @param from index of the node from which the edge originates + * @param to index of the node the edge is going to + * @param weight the weight of the edge + * @returns a bool indicating if adding the edge was successful + */ + auto addEdge(const size_t from, size_t to, double weight) -> bool { + if (from < nodeNumber && to < nodeNumber && from != to) { + adjacencies_[from].emplace_back(std::pair(to, weight)); + return true; + } + return false; + } + + /** + * @brief Adds an edge between two nodes to the graph (weight 1.0) + * @param from index of the node from which the edge originates + * @param to index of the node the edge is going to + * @returns a bool indicating if adding the edge was successful + */ + auto addEdge(size_t from, size_t to) -> bool { + if (from < nodeNumber && to < nodeNumber && from != to) { + adjacencies_[from].emplace_back(std::pair(to, 1.0)); + return true; + } + return false; + } + + /** + * @brief Gets the value of a given node + * @param node the node index of a node in the graph + * @returns an object of type T contained in the given node + */ + auto getNodeValue(size_t node) -> T { + if (node < nodeNumber) { + return nodeValues_[node]; + } else { + std::ostringstream oss; + oss << "ERROR: Node Number out of range: " << node << "\n"; + throw std::invalid_argument(oss.str()); + } + } + + /** + * @brief A function which returns the size/number of nodes of the graph + * @returns the number of nodes in the graph + */ + [[nodiscard]] auto size() const -> size_t { return nodeNumber; } + + /** + * @brief Returns the successor nodes of a given node + * @param node 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(size_t node) const + -> std::vector> { + return adjacencies_.at(node); + } + }; + /** + * @brief converts a schedule of operations into a directional acyclic graph, + * 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 a pair of vectors containing layers of StructU3's + * representing U3-Gates and TwoQubitGateLayers + * @param nQubits 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) + -> DiGraph>>; + /** + * @brief Recursively finds the cheapest path to the start node of the + * subproblem graph from a set of leaf nodes. + * @details + * @param subproblemGraph the subproblem graph to find the path in + * @param currentNode the node of the current function call + * @param leafNodes a set of nodes with no outgoing edges (aka. leaf nodes) + * @param memo + * @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 moment) + */ + static auto cheapestPathToStart( + const DiGraph, std::vector>>& + subproblemGraph, + size_t currentNode, const std::set& leafNodes, + std::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 + * @details + * @param subproblemGraph the subproblem graph + * @param leafNodes a vector containing the indices of all leaf nodes of the + * graph + * @returns a vector containing the node inidces of the cheapest path through + * the graph + */ + static auto findCheapestPath( + const DiGraph, std::vector>>& + subproblemGraph, + const std::vector& leafNodes) -> std::vector; + + /** + * @brief Finds the leaf nodes (Nodes with no outgoing edges) of a subproblem + * graph + * @details + * @param subproblemGraph the subproblem graph + * @returns a vectr of node indices for the leaf nodes + */ + static auto findLeafNodes( + const DiGraph, std::vector>>& + subproblemGraph) -> std::vector; + + /** + * @brief Removes all copies of an element from a vector + * @param vector the vector of size_t to remove the element from + * @param elem the element to be removed from the vector + * @returns the vector without the element + */ + static auto removeElement(const std::vector& vector, size_t elem) + -> std::vector; + + /** + * @brief Returns all plausible subsets of the current moments to be scheduled + * @details + * @param circuit the graph representation of the quantum circuit + * @param v0Current a vector containing the node indices of the current set of + * single Qubit operations + * @param vNew =[v_p1,v_c1, v_rem] an array containing vectors holding the + * node indices of the next set of two Qubit operations (v_p), single Qubit + * operations (v_c) and all remaining operations (v_rem) + * @param checkFinalCond a bool deciding whether to check for a strict cost + * reduction + * @returns a vector holding pairs of the possible next moments to be + * scheduled [v_c0, v_p1,vc_1,v_rem] and the moments associated + */ + static auto getPossibleMoments( + DiGraph>>& circuit, + const std::vector& v0Current, + const std::array, 3>& vNew, bool checkFinalCond) + -> std::vector, 4>, qc::fp>>; + + /** + * @brief Finds the maximal value of the angle theta among the given set of + * nodes + * @param circuit the passed circuit graph containing operations + * @param nodes a vector of node indices for which to find the maximal theta + * @returns the maximal theta value + */ + static auto + maxTheta(DiGraph>>& circuit, + const std::vector& nodes) -> qc::fp; + + /** + * @brief returns the next two- and single-Qubit moments which can be + * scheduled + * @details + * @param circuit the quantum circuit in graph form + * @param v a vector containing all unscheduled nodes + * @param nQubits the number of qubits in the circuit + * @returns an array containing vectors of the next two Qubit moments which + * can be scheduled and the remaining nodes: [v_p,v_c,v_rem] v_p: next two + * qubit gate moment v_c: next single qubit gate moment V-rem: remaining + * unscheduled nodes + */ + static auto + sift(DiGraph>>& circuit, + std::vector v, size_t nQubits) + -> std::array, 3>; + + /** + * @brief Builds a schedule from a circuit and subproblem graph + * @details + * @param circuit the circuit to be scheduled in graph form + * @param subproblemGraph the subproblem graph of the circuit + * @returns a pair of vectrs containing layers of StructU3's and two element + * arrays of Qubits representing CZ gates making up a schedule + */ + static auto buildSchedule( + DiGraph>>& circuit, + DiGraph, std::vector>>& + subproblemGraph) -> std::pair>, + std::vector>; + + /** + * @brief Adds a node corresponding to the subproblem [v_p,v_c] to the + * subproblem graph + * @details + * @param vp a vector of node indices making up a two-Qubit gate moment + * @param vc a vector of node indices making up a single-Qubit gate moment + * @param cost the maximal theta value of operations in vc (aka. the cost) + * @param subproblemGraph a subproblem graph of a circuit + * @param prevNode 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& vp, const std::vector& vc, qc::fp cost, + DiGraph, std::vector>>& + subproblemGraph, + size_t prevNode) -> size_t; + + /** + * @brief Recursively creates a subproblem graph for a given circuit + * @details + * @param v the current subproblem [v_p,v_c,v_rem] for which to create a + * schedule + * @param circuit the graph representation of the circuit to be scheduled + * @param subproblemGraph the subproblem graph of the circuit to be scheduled + * @param prevNode the previous node in the subproblem graph + * @param nQubits the number of qubits in the circuit + * @param checkFinalCond a bool deciding whether the function should only + * allow possible next moments with strictly decreasing cost + * @param memo a map using subproblem hashes as keys and saving as values 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 + */ + static auto scheduleRemaining( + const std::array, 3>& v, + DiGraph>>& circuit, + DiGraph, std::vector>>& + subproblemGraph, + size_t prevNode, size_t nQubits, bool checkFinalCond, + std::map>>& memo) + -> double; + + /** + * @brief Creates a schedule minimizing the sum total of the global rotation + * angles theta across a quantum circuit + * @details + * @param schedule the preliminary schedule provided + * @param nQubits 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 +/** + * A hash function for subproblems [v_p,v_c,v_rem] + */ +template <> struct std::hash, 3>> { + auto + operator()(const std::array, 3>& array) const noexcept + -> size_t { + size_t seed = 0U; + for (const auto& v : array) { + for (auto node : v) { + qc::hashCombine(seed, std::hash{}(node)); + } + } + return seed; + } +}; diff --git a/include/na/zoned/decomposer/NoOpDecomposer.hpp b/include/na/zoned/decomposer/NoOpDecomposer.hpp index d0a9edef1..fb6a8d788 100644 --- a/include/na/zoned/decomposer/NoOpDecomposer.hpp +++ b/include/na/zoned/decomposer/NoOpDecomposer.hpp @@ -49,7 +49,8 @@ class NoOpDecomposer : public DecomposerBase { } [[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..8052ef49c 100644 --- a/python/mqt/qmap/na/zoned.pyi +++ b/python/mqt/qmap/na/zoned.pyi @@ -219,3 +219,89 @@ class RoutingAwareCompiler: 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 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, float]: + """Get the statistics of the last compilation. + + Returns: + The statistics as a dictionary + """ diff --git a/src/na/zoned/decomposer/NativeGateDecomposer.cpp b/src/na/zoned/decomposer/NativeGateDecomposer.cpp new file mode 100644 index 000000000..969708fe8 --- /dev/null +++ b/src/na/zoned/decomposer/NativeGateDecomposer.cpp @@ -0,0 +1,722 @@ +/* + * 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 + +namespace na::zoned { + +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 { + qc::fp thetaMax = 0; + for (auto [angles, qubit] : layers) { + if (std::fabs(angles.theta) > thetaMax) { + thetaMax = std::fabs(angles.theta); + } + } + return thetaMax; +} +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 { + assert(gate.get().getNqubits() != 1 && + "Gate has to be a single qubit gate."); + gatesPerQubit[gate.get().getTargets().front()].emplace_back(gate); + }); + std::vector newLayer; + std::ranges::transform( + std::views::iota(gatesPerQubit.size()) | + std::views::filter([&gatesPerQubit](const auto i) -> bool { + return !gatesPerQubit[i].empty(); + }), + std::back_inserter(newLayer), + [&gatesPerQubit](const auto i) -> StructU3 { + const auto& gates = gatesPerQubit[i]; + const auto& quat = std::accumulate( + gates.begin(), gates.end(), Quaternion{}, + [](Quaternion q, const auto& gate) -> Quaternion { + return combineQuaternions(std::move(q), + convertGateToQuaternion(gate)); + }); + const auto& angles = getU3AnglesFromQuaternion(quat); + return StructU3{.angles = angles, + .qubit = gates.front().get().getTargets().front()}; + }); + newLayers.emplace_back(newLayer); + } + return newLayers; +} +auto NativeGateDecomposer::getDecompositionAngles(const Angles& angles, + const qc::fp thetaMax) + -> Angles { + qc::fp alpha; + qc::fp chi; + // 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 sinSqDiff = sin(thetaMax / 2) * sin(thetaMax / 2) - + sin(angles.theta / 2) * sin(angles.theta / 2); + if (std::fabs(sinSqDiff) < epsilon) { + chi = qc::PI; + if (std::fabs(cos(thetaMax / 2)) < epsilon) { // Periodicity covered? + alpha = 0; + } else { + alpha = qc::PI_2; + } + } else { + const auto kappa = + std::sqrt((sin(angles.theta / 2) * sin(angles.theta / 2)) / sinSqDiff); + alpha = atan(cos(thetaMax / 2) * kappa); + chi = fmod(2 * atan(kappa), qc::TAU); + } + const auto beta = angles.theta < 0 ? -1 * qc::PI_2 : qc::PI_2; + + return {.theta = beta, + .phi = fmod(angles.phi - (alpha - beta), qc::TAU), + .lambda = fmod(angles.lambda - (alpha + beta), qc::TAU)}; +} + +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 schedule = + scheduleThetaOpt(std::pair(u3Layers, twoQubitGateLayers), nQubits); + u3Layers = schedule.first; + newTwoQubitLayers = schedule.second; + } else { + newTwoQubitLayers = twoQubitGateLayers; + } + std::vector newSingleQubitLayers; + for (const auto& layer : u3Layers) { + const auto thetaMax = calcThetaMax(layer); + SingleQubitGateLayer frontLayer; + SingleQubitGateLayer midLayer; + SingleQubitGateLayer backLayer; + auto& newLayer = newSingleQubitLayers.emplace_back(); + + 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})); + } + if (!layer.empty()) { + 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 DiGraph, + std::vector>>& subproblemGraph, + const std::vector& leafNodes) -> std::vector { + const std::set leaves(leafNodes.begin(), leafNodes.end()); + // Memory Map : Sub-problem nodes as keys + std::map, qc::fp>> memo; + auto [path, cost] = cheapestPathToStart(subproblemGraph, 0, leaves, memo); + path.resize(path.size() - 1); + std::ranges::reverse(path); + return path; +} +auto disjunct(const std::set& set1, const std::set& set2) + -> bool { + return std::ranges::all_of(set1, + [&](auto elem) { return !set2.contains(elem); }); +} + +auto disjunct(const std::set& set1, const std::set& set2) + -> bool { + return std::ranges::all_of(set1, + [&](auto elem) { return !set2.contains(elem); }); +} + +auto NativeGateDecomposer::cheapestPathToStart( + const DiGraph, + std::vector>>& subproblemGraph, + std::size_t currentNode, const std::set& leafNodes, + std::map, qc::fp>>& memo) + -> std::pair, double> { + std::vector, double>> possiblePaths; + // Check if leaf nodes are reached + if (memo.contains(currentNode)) { + return memo.at(currentNode); + } + // Base Case: + for (auto [source, target] : subproblemGraph.getAdjacent(currentNode)) { + if (leafNodes.contains(source)) { + possiblePaths.emplace_back(std::vector{source, currentNode}, target); + } + } + // Recursive Case + if (possiblePaths.empty()) { + for (auto [source, target] : subproblemGraph.getAdjacent(currentNode)) { + auto path = cheapestPathToStart(subproblemGraph, source, leafNodes, memo); + path.first.emplace_back(currentNode); + path.second += target; + possiblePaths.emplace_back(path); + } + } + // 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) { return a.second < b.second; }); + memo[currentNode] = bestPathWithCost; + return bestPathWithCost; +} + +auto NativeGateDecomposer::findLeafNodes( + const DiGraph, + std::vector>>& subproblemGraph) + -> std::vector { + std::vector leafNodes; + std::ranges::copy(std::views::iota(subproblemGraph.size()) | + std::views::filter([&subproblemGraph](auto i) -> bool { + return subproblemGraph.getAdjacent(i).empty(); + }), + std::back_inserter(leafNodes)); + return leafNodes; +} + +auto NativeGateDecomposer::removeElement(const std::vector& vector, + const std::size_t elem) + -> std::vector { + std::vector newVector; + for (auto element : vector) { + if (element != elem) { + newVector.emplace_back(element); + } + } + return newVector; +} + +auto NativeGateDecomposer::getPossibleMoments( + DiGraph>>& circuit, + const std::vector& v0Current, + const std::array, 3>& vNew, bool checkFinalCond) + -> std::vector, 4>, qc::fp>> { + + std::vector vP1Star = vNew[0]; + std::vector vP1Square = {}; + std::vector vc1Star = vNew[1]; + std::vector vc1Square = {}; + + qc::fp vc0Cost = maxTheta(circuit, v0Current); + qc::fp vc1Cost = maxTheta(circuit, vNew[1]); + qc::fp origCombCost = vc0Cost + vc1Cost; + qc::fp newVc1Cost = std::max(vc0Cost, vc1Cost); + + std::array, 4> vArg = {v0Current, vNew[0], vNew[1], + vNew[2]}; + std::vector, 4>, qc::fp>> args = + {std::pair(vArg, vc0Cost)}; + // Sort v_0C from highest to lowest theta + std::vector vSort(v0Current); + auto sortByTheta = [&circuit](std::size_t a, std::size_t b) -> bool { + return std::fabs(std::get(circuit.getNodeValue(a)).angles.theta) > + std::fabs(std::get(circuit.getNodeValue(b)).angles.theta); + }; + std::ranges::sort(vSort, sortByTheta); + // TODO: Check Condition 1 + std::vector, 2>, + std::pair, qc::fp>>> + potentialArg = {}; + auto prevTheta = std::fabs( + std::get(circuit.getNodeValue(vSort[0])).angles.theta); + auto thisTheta = prevTheta; + std::set mkQubits = { + std::get(circuit.getNodeValue(vSort[0])).qubit}; + for (auto i = 0; static_cast(i) < vSort.size(); i++) { + thisTheta = std::fabs( + std::get(circuit.getNodeValue(vSort[static_cast(i)])) + .angles.theta); + if (thisTheta != prevTheta) { + std::vector discarded = {vSort.begin(), vSort.begin() + i}; + std::vector kept = {vSort.begin() + i, vSort.end()}; + potentialArg.emplace_back( + std::pair, 2>, + std::pair, qc::fp>>( + {kept, discarded}, + std::pair, qc::fp>(mkQubits, thisTheta))); + prevTheta = thisTheta; + mkQubits.clear(); + } + mkQubits.insert( + std::get(circuit.getNodeValue(vSort[static_cast(i)])) + .qubit); + } + std::vector emplaceBackNodes = {}; + std::set pSquareQubits = {}; + + for (auto pot : potentialArg) { + // TODO: Check Condition 2 + for (auto node : vP1Star) { + std::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; + } + // TODO: Check Condition 3 + std::set pushQubits = pot.second.first; + pushQubits.merge(pSquareQubits); + emplaceBackNodes.clear(); + + for (auto node : vc1Star) { + std::set qubits = {std::get(circuit.getNodeValue(node)).qubit}; + if (!disjunct(qubits, pushQubits)) { + emplaceBackNodes.emplace_back(node); + } + } + for (auto node : emplaceBackNodes) { + vc1Star = removeElement(vc1Star, node); + vc1Square.emplace_back(node); + } + + if (vc1Star.empty()) { + break; + } + // TODO Check Condition 4 + if (!checkFinalCond || pot.second.second + newVc1Cost < origCombCost) { + vArg = {pot.first[0], vP1Star, pot.first[1], vNew[2]}; + for (auto node : vc1Star) { + vArg[2].emplace_back(node); + } + for (auto node : vc1Square) { + vArg[3].emplace_back(node); + } + for (auto node : vP1Square) { + vArg[3].emplace_back(node); + } + args.emplace_back(vArg, pot.second.second); + } + } + return args; +} + +auto NativeGateDecomposer::convertCircuitToDAG( + const std::pair>, + std::vector>& schedule, + std::size_t nQubits) + -> DiGraph>> { + // std::variant> instead of Unique_pointer + // For Readout: + DiGraph>> graph = + DiGraph>>(); + 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.add_Node(s); + qubitPaths.at(s.qubit).emplace_back(node); + } + + for (const auto& gate : schedule.second.at(i)) { + size_t node = graph.add_Node(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.add_Node(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( + DiGraph>>& 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( + DiGraph>>& circuit, + std::vector v, size_t nQubits) + -> std::array, 3> { + std::vector vp = std::vector(); + std::vector v_c = std::vector(); + std::vector vr = std::vector(); + + std::set vRemaining = std::set(v.begin(), v.end()); + std::set removed = std::set(); + + // We traverse the graph rather than v_rem to use the graph's topological + // ordering + for (size_t node = 0; node < circuit.size(); node++) { + if (vRemaining.contains(node)) { + auto op = circuit.getNodeValue(node); + std::set opQubits = std::set(); + + if (std::holds_alternative(op)) { + opQubits = {std::get(op).qubit}; + } else { + opQubits = {std::get>(op)[0], + std::get>(op)[1]}; + } + if (removed.size() < nQubits && disjunct(removed, opQubits)) { + if (std::holds_alternative(op)) { + v_c.emplace_back(node); + removed.insert(std::get(op).qubit); + } else { + vp.emplace_back(node); + } + } else { + vr.emplace_back(node); + for (auto qubit : opQubits) { + removed.insert(qubit); + } + } + } + } + return std::array, 3>{{vp, v_c, vr}}; +} + +auto NativeGateDecomposer::buildSchedule( + DiGraph>>& circuit, + DiGraph, std::vector>>& + subproblemGraph) -> std::pair>, + std::vector> { + + std::vector leafNodes = findLeafNodes(subproblemGraph); + std::vector minimalPath = + findCheapestPath(subproblemGraph, leafNodes); + std::pair>, std::vector> + schedule = std::pair>, + std::vector>{}; + + std::vector singleQubitGates; + std::vector> twoQubitGates; + + if (!subproblemGraph.getNodeValue(minimalPath[0]).first.empty()) { + schedule.first.emplace_back(); + } + std::set usedQubits{}; + + for (std::size_t i = 0; i < minimalPath.size(); i++) { + singleQubitGates.clear(); + twoQubitGates.clear(); + usedQubits.clear(); + + for (auto j : subproblemGraph.getNodeValue(minimalPath[i]).first) { + auto op = circuit.getNodeValue(j); + if (std::holds_alternative>(op)) { + // TODO: Check if TWOQUBIT GATES Can be executed in parallel!! + 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 (auto j : subproblemGraph.getNodeValue(minimalPath[i]).second) { + auto op = circuit.getNodeValue(j); + if (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& vp, const std::vector& vc, qc::fp cost, + DiGraph, std::vector>>& + subproblemGraph, + std::size_t prevNode) -> size_t { + std::size_t newNode = subproblemGraph.add_Node(std::pair(vp, vc)); + subproblemGraph.addEdge(prevNode, newNode, cost); + return newNode; +} + +auto NativeGateDecomposer::scheduleRemaining( + const std::array, 3>& v, + DiGraph>>& circuit, + DiGraph, std::vector>>& + subproblemGraph, + size_t prevNode, size_t nQubits, bool checkFinalCond, + std::map>>& memo) + -> double { + double cost; + // TODO: Check if subproblem has been computed + std::size_t id = std::hash, 3>>{}(v); + if (memo.contains(id)) { + std::size_t subNode = memo.at(id).first; + double edgeWeight = memo.at(id).second[1]; + cost = memo.at(id).second[0]; + subproblemGraph.addEdge(prevNode, subNode, edgeWeight); + return cost; + } + // TODO: Base Case-> V_rem is empty + if (v[2].empty()) { + // TODO:Decide if I need if to check for TWO QUBIT + if (v[1].empty()) { + cost = 0; + } else { + cost = std::fabs( + std::get(circuit.getNodeValue(v[1].at(0))).angles.theta); + } + for (std::size_t i : v[1]) { + if (std::get(circuit.getNodeValue(i)).angles.theta > cost) { + cost = + std::fabs(std::get(circuit.getNodeValue(i)).angles.theta); + } + } + auto end_node = + addNodeToSubproblemGraph(v[0], v[1], cost, subproblemGraph, prevNode); + memo[id] = std::pair>( + end_node, {cost, cost}); // TODO: Correct to put cost for both??? + return cost; + } + // TODO: Recursive Call: Only + auto vNew = sift(circuit, v[2], nQubits); + auto args = getPossibleMoments(circuit, v[1], vNew, checkFinalCond); + qc::fp tempCost = 0; + double minCost = std::numeric_limits::max(); + double minWeight = std::numeric_limits::max(); + std::size_t minNode; + for (const auto& val : args) { + auto newNode = addNodeToSubproblemGraph(v[0], val.first[0], val.second, + subproblemGraph, prevNode); + tempCost = scheduleRemaining({val.first[1], val.first[2], val.first[3]}, + circuit, subproblemGraph, newNode, nQubits, + checkFinalCond, memo) + + val.second; + if (tempCost < minCost) { + minCost = tempCost; + minNode = newNode; + minWeight = val.second; + } + } + memo[id] = std::pair>( + minNode, {minCost, minWeight}); + return minCost; +} + +auto NativeGateDecomposer::scheduleThetaOpt( + const std::pair>, + std::vector>& schedule, + std::size_t nQubits) const -> std::pair>, + std::vector> { + + // TODO: Convert Circuit to DAG: How to handle the unique Pointer situation??? + DiGraph>> circuit = + convertCircuitToDAG(schedule, nQubits); + // TODO: Get initial Moments( Not does MQB THEN SQB!! SOl to get SQB MQB??) + std::vector v_start{}; + v_start.reserve(circuit.size()); + for (size_t i = 0; i < circuit.size(); ++i) { + v_start.emplace_back(i); + } + // v=(v_p,v_c,v_r) + std::array, 3> v = sift(circuit, v_start, nQubits); + // TODO: Create Subproblem Graph + DiGraph, std::vector>> + subproblemGraph = DiGraph< + std::pair, std::vector>>(); + // TODO: First Call of Recursive Function to create Schedule + auto baseNode = subproblemGraph.add_Node( + std::pair, std::vector>({}, {})); + std::map>> memo = + {}; + auto cost = scheduleRemaining(v, circuit, subproblemGraph, baseNode, nQubits, + config_.checkFinalCond, memo); + // TODO: Create Schedule from Subproblem Graph + std::pair>, std::vector> + finalCircuit = buildSchedule(circuit, subproblemGraph); + return finalCircuit; +} +} // 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/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_native_gate_decomposer.cpp b/test/na/zoned/test_native_gate_decomposer.cpp new file mode 100644 index 000000000..9a0ecf4e5 --- /dev/null +++ b/test/na/zoned/test_native_gate_decomposer.cpp @@ -0,0 +1,623 @@ +/* + * 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 + */ + +// +// Created by cpsch on 16.12.2025. +// + +#include "ir/QuantumComputation.hpp" +#include "na/zoned/decomposer/NativeGateDecomposer.hpp" +#include "na/zoned/scheduler/ASAPScheduler.hpp" + +#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 DecomposerTest : public ::testing::Test { +protected: + Architecture architecture; + ASAPScheduler::Config schedulerConfig{.maxFillingFactor = .8}; + ASAPScheduler scheduler; + NativeGateDecomposer::Config decomposerConfig{}; + NativeGateDecomposer decomposer; + DecomposerTest() + : architecture(Architecture::fromJSONString(architectureJson)), + scheduler(architecture, schedulerConfig), + decomposer(architecture, decomposerConfig) {} +}; + +constexpr static qc::fp epsilon = std::numeric_limits::epsilon() * 1024; + +// Test Translation of : S gate, Sdg Gate, T-gate, t dg gate, U2, RY, Y, Vdg, +// SX, Sxdg, Unrecognized, H _>Just do them all? + +TEST(Test, ZRotGateTranslationTest) { + + qc::StandardOperation op = qc::StandardOperation(0, qc::Z); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1, epsilon))); + + op = qc::StandardOperation(0, qc::RZ, {qc::PI_2}); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1 / std::sqrt(2), epsilon))); + op = qc::StandardOperation(0, qc::P, {qc::PI_2}); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1 / std::sqrt(2), epsilon))); + + op = qc::StandardOperation(0, qc::S); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1 / std::sqrt(2), epsilon))); + + op = qc::StandardOperation(0, qc::Sdg); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre( + ::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(-1 / std::sqrt(2), epsilon))); + op = qc::StandardOperation(0, qc::T); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre( + ::testing::DoubleNear(0.5 * std::sqrt(2 + std::sqrt(2)), epsilon), + ::testing::DoubleNear(0, epsilon), ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0.5 * std::sqrt(2 - std::sqrt(2)), epsilon))); + + op = qc::StandardOperation(0, qc::Tdg); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre( + ::testing::DoubleNear(0.5 * std::sqrt(2 + std::sqrt(2)), epsilon), + ::testing::DoubleNear(0, epsilon), ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(-0.5 * std::sqrt(2 - std::sqrt(2)), epsilon))); +} + +TEST(Test, XYRotGateTranslationTest) { + qc::StandardOperation op = qc::StandardOperation(0, qc::X); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon))); + + op = qc::StandardOperation(0, qc::RX, {qc::PI_2}); + + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon))); + + op = qc::StandardOperation(0, qc::Y); + + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1, epsilon), + ::testing::DoubleNear(0, epsilon))); + + op = qc::StandardOperation(0, qc::RY, {qc::PI_2}); + + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon))); + + op = qc::StandardOperation(0, qc::SX); + + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon))); + + op = qc::StandardOperation(0, qc::SXdg); + + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(-1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon))); +} + +TEST(Test, UGateTranslationTest) { + qc::fp p = qc::PI_2; + qc::fp t = qc::PI_4; + qc::fp l = qc::PI_4; + qc::StandardOperation op = qc::StandardOperation(0, qc::U, {t, p, l}); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre( + ::testing::DoubleNear( + (std::cos(p / 2) * std::cos(t / 2) * std::cos(l / 2)) - + (std::sin(p / 2) * std::cos(t / 2) * std::sin(l / 2)), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::sin(t / 2) * std::sin(l / 2) - + std::sin(p / 2) * std::cos(l / 2) * std::sin(t / 2), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::sin(t / 2) * std::cos(l / 2) + + std::sin(p / 2) * std::sin(l / 2) * std::sin(t / 2), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::cos(t / 2) * std::sin(l / 2) + + std::sin(p / 2) * std::cos(l / 2) * std::cos(t / 2), + epsilon))); + + t = qc::PI_2; + op = qc::StandardOperation(0, qc::U2, {p, l}); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre( + ::testing::DoubleNear( + (std::cos(p / 2) * std::cos(t / 2) * std::cos(l / 2)) - + (std::sin(p / 2) * std::cos(t / 2) * std::sin(l / 2)), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::sin(t / 2) * std::sin(l / 2) - + std::sin(p / 2) * std::cos(l / 2) * std::sin(t / 2), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::sin(t / 2) * std::cos(l / 2) + + std::sin(p / 2) * std::sin(l / 2) * std::sin(t / 2), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::cos(t / 2) * std::sin(l / 2) + + std::sin(p / 2) * std::cos(l / 2) * std::cos(t / 2), + epsilon))); + + t = -1 * qc::PI_2; + l = -1 * qc::PI_2; + + op = qc::StandardOperation(0, qc::Vdg); + EXPECT_THAT(NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre( + ::testing::DoubleNear( + (std::cos(p / 2) * std::cos(t / 2) * std::cos(l / 2)) - + (std::sin(p / 2) * std::cos(t / 2) * std::sin(l / 2)), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::sin(t / 2) * std::sin(l / 2) - + std::sin(p / 2) * std::cos(l / 2) * std::sin(t / 2), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::sin(t / 2) * std::cos(l / 2) + + std::sin(p / 2) * std::sin(l / 2) * std::sin(t / 2), + epsilon), + ::testing::DoubleNear( + std::cos(p / 2) * std::cos(t / 2) * std::sin(l / 2) + + std::sin(p / 2) * std::cos(l / 2) * std::cos(t / 2), + epsilon))); + op = qc::StandardOperation(0, qc::H); + EXPECT_THAT( + NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(op)), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1 / std::sqrt(2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(1 / std::sqrt(2), epsilon))); +} + +TEST(Test, ThreeQuaternionCombiTest) { + std::array q1 = {cos(qc::PI_4), 0, 0, sin(qc::PI_4)}; + std::array q2 = {cos(qc::PI_2), 0, sin(qc::PI_2), 0}; + std::array q12 = NativeGateDecomposer::combineQuaternions(q1, q2); + EXPECT_THAT(q12, ::testing::ElementsAre( + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(-1 * cos(qc::PI_4), epsilon), + ::testing::DoubleNear(cos(qc::PI_4), epsilon), + ::testing::DoubleNear(0, epsilon))); + std::array q3 = {cos(qc::PI_2), 0, 0, sin(qc::PI_2)}; + std::array q13 = NativeGateDecomposer::combineQuaternions(q12, q3); + EXPECT_THAT( + q13, ::testing::ElementsAre(::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(cos(qc::PI_4), epsilon), + ::testing::DoubleNear(cos(qc::PI_4), epsilon), + ::testing::DoubleNear(0, epsilon))); +} + +TEST(Test, ThreeQuaternionU3Test) { + std::array q1 = {cos(qc::PI_2), 0, 0, sin(qc::PI_2)}; + std::array q2 = {cos(qc::PI_4 / 2), 0, sin(qc::PI_4 / 2), 0}; + std::array q12 = NativeGateDecomposer::combineQuaternions(q1, q2); + EXPECT_THAT(q12, ::testing::ElementsAre( + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(-1 * sin(qc::PI_4 / 2), epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(cos(qc::PI_4 / 2), epsilon))); + std::array q3 = {cos(qc::PI_4), 0, 0, sin(qc::PI_4)}; + std::array q13 = NativeGateDecomposer::combineQuaternions(q12, q3); + qc::fp r2 = 1 / std::sqrt(2); + EXPECT_THAT(q13, ::testing::ElementsAre( + ::testing::DoubleNear(-r2 * cos(qc::PI_4 / 2), epsilon), + ::testing::DoubleNear(-r2 * sin(qc::PI_4 / 2), epsilon), + ::testing::DoubleNear(r2 * sin(qc::PI_4 / 2), epsilon), + ::testing::DoubleNear(r2 * cos(qc::PI_4 / 2), epsilon))); +} + +TEST(Test, SingleXGateAngleTest) { + const qc::Operation* op = new qc::StandardOperation(0, qc::X); + std::array q = NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(*op)); + EXPECT_THAT(NativeGateDecomposer::getU3AnglesFromQuaternion(q), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(qc::PI, epsilon))); +} + +TEST(Test, SingleU3GateAngleTest) { + const qc::Operation* op = + new qc::StandardOperation(0, qc::U, {qc::PI_4, qc::PI, qc::PI_2}); + std::array q = NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(*op)); + qc::fp r2 = 1 / sqrt(2); + EXPECT_THAT(q, ::testing::ElementsAre( + ::testing::DoubleNear(-r2 * cos(qc::PI_4 / 2), epsilon), + ::testing::DoubleNear(-r2 * sin(qc::PI_4 / 2), epsilon), + ::testing::DoubleNear(r2 * sin(qc::PI_4 / 2), epsilon), + ::testing::DoubleNear(r2 * cos(qc::PI_4 / 2), epsilon))); + + EXPECT_THAT(NativeGateDecomposer::getU3AnglesFromQuaternion(q), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_4, epsilon), + ::testing::DoubleNear(qc::PI, epsilon), + ::testing::DoubleNear(qc::PI_2, epsilon))); +} + +TEST(Test, ThetaPiAngleTest) { + const qc::Operation* op = + new qc::StandardOperation(0, qc::U, {qc::PI, qc::PI, qc::PI_2}); + std::array q = NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(*op)); + qc::fp r2 = 1 / sqrt(2); + EXPECT_THAT(q, ::testing::ElementsAre(::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(-r2, epsilon), + ::testing::DoubleNear(r2, epsilon), + ::testing::DoubleNear(0, epsilon))); + EXPECT_THAT( + NativeGateDecomposer::getU3AnglesFromQuaternion(q), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(-1 * qc::PI_2, epsilon))); +} + +TEST(Test, ThetaZeroAngleTest) { + const qc::Operation* op = + new qc::StandardOperation(0, qc::U, {0, qc::PI, qc::PI_2}); + std::array q = NativeGateDecomposer::convertGateToQuaternion( + std::reference_wrapper(*op)); + qc::fp r2 = 1 / sqrt(2); + EXPECT_THAT(q, ::testing::ElementsAre(::testing::DoubleNear(-r2, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(r2, epsilon))); + + EXPECT_THAT( + NativeGateDecomposer::getU3AnglesFromQuaternion(q), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(3 * qc::PI_2, epsilon))); +} + +TEST(Test, RXDecompositionTest) { + std::array rx = {qc::PI, -qc::PI_2, qc::PI_2}; + EXPECT_THAT(NativeGateDecomposer::getDecompositionAngles(rx, qc::PI), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon))); +} + +TEST(Test, U3DecompositionTest) { + std::array u3 = {qc::PI_4, qc::PI, qc::PI_2}; + EXPECT_THAT( + NativeGateDecomposer::getDecompositionAngles(u3, qc::PI_4), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon), + ::testing::DoubleNear(qc::PI, epsilon), + ::testing::DoubleNear(-qc::PI_2, epsilon))); +} + +TEST(Test, DoubleDecompositionTest) { + std::array x1 = {qc::PI, -qc::PI_2, qc::PI_2}; + std::array z2 = {0, 0, qc::PI}; + EXPECT_THAT(NativeGateDecomposer::getDecompositionAngles(x1, qc::PI), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon), + ::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(0, epsilon))); + EXPECT_THAT(NativeGateDecomposer::getDecompositionAngles(z2, qc::PI), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon), + ::testing::DoubleNear(qc::PI_2, epsilon), + ::testing::DoubleNear(qc::PI_2, epsilon))); +} + +TEST_F(DecomposerTest, SingleRXGate) { + // ┌───────┐ + // q: ┤ Rx(π) ├ + // └───────┘ + size_t n = 1; + qc::QuantumComputation qc(n); + qc.rx(qc::PI, 0); + const auto& schedule = scheduler.schedule(qc); + auto decomp = decomposer.decompose(qc.getNqubits(), schedule); + EXPECT_EQ(decomp.first.size(), 1); + EXPECT_EQ(decomp.first[0].size(), 5); + EXPECT_EQ(decomp.first[0][0]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][0]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp.first[0][0]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + EXPECT_TRUE(decomp.first[0][1]->isCompoundOperation()); + EXPECT_TRUE(decomp.first[0][1]->isGlobal(n)); + EXPECT_EQ(decomp.first[0][2]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][2]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp.first[0][2]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon))); + EXPECT_TRUE(decomp.first[0][3]->isCompoundOperation()); + EXPECT_TRUE(decomp.first[0][3]->isGlobal(n)); + EXPECT_EQ(decomp.first[0][4]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][4]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp.first[0][4]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); +} + +TEST_F(DecomposerTest, SingleU3Gate) { + // ┌─────────────┐ + // q: ┤ U3(0,π,π/2) ├ + // └─────────────┘ + size_t n = 1; + qc::QuantumComputation qc(n); + qc.u(0.0, qc::PI, qc::PI_2, 0); + const auto& sched = scheduler.schedule(qc); + auto decomp = decomposer.decompose(qc.getNqubits(), sched); + EXPECT_EQ(decomp.first.size(), 1); + EXPECT_EQ(decomp.first[0].size(), 5); + + EXPECT_EQ(decomp.first[0][0]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][0]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp.first[0][0]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon))); + EXPECT_TRUE(decomp.first[0][1]->isCompoundOperation()); + EXPECT_TRUE(decomp.first[0][1]->isGlobal(n)); + EXPECT_EQ(decomp.first[0][2]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][2]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp.first[0][2]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon))); + EXPECT_TRUE(decomp.first[0][3]->isCompoundOperation()); + EXPECT_TRUE(decomp.first[0][3]->isGlobal(n)); + EXPECT_EQ(decomp.first[0][4]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][4]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp.first[0][4]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); +} + +TEST_F(DecomposerTest, TwoPauliGatesOneQubit) { + // ┌───────┐ ┌───────┐ + // q: ┤ X ├──┤ Z ├ + // └───────┘ └───────┘ + size_t n = 1; + qc::QuantumComputation qc(n); + qc.x(0); + qc.z(0); + const auto& sched = scheduler.schedule(qc); + auto decomp = decomposer.decompose(qc.getNqubits(), sched); + + EXPECT_EQ(decomp.first.size(), 1); + EXPECT_EQ(decomp.first[0].size(), 5); + EXPECT_EQ(decomp.first[0][0]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][0]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp.first[0][0]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + EXPECT_TRUE(decomp.first[0][1]->isCompoundOperation()); + EXPECT_TRUE(decomp.first[0][1]->isGlobal(n)); + EXPECT_EQ(decomp.first[0][2]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][2]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp.first[0][2]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon))); + EXPECT_TRUE(decomp.first[0][3]->isCompoundOperation()); + EXPECT_TRUE(decomp.first[0][3]->isGlobal(n)); + EXPECT_EQ(decomp.first[0][4]->getType(), qc::RZ); + EXPECT_THAT(decomp.first[0][4]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT( + decomp.first[0][4]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(3 * qc::PI_2, epsilon))); +} + +TEST_F(DecomposerTest, TwoPauliGatesTwoQubits) { + // ┌───────┐ + // q_0: ─┤ X ├─ + // └───────┘ + // ┌───────┐ + // q_1: ─┤ Z ├─ + // └───────┘ + + size_t n = 2; + qc::QuantumComputation qc(n); + qc.x(0); + qc.z(1); + const auto& sched = scheduler.schedule(qc); + auto decomp = decomposer.decompose(qc.getNqubits(), sched).first; + EXPECT_EQ(decomp.size(), 1); + EXPECT_EQ(decomp[0].size(), 8); + + EXPECT_EQ(decomp[0][0]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][0]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[0][0]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + + EXPECT_EQ(decomp[0][1]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][1]->getTargets(), ::testing::ElementsAre(1)); + EXPECT_THAT(decomp[0][1]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + + EXPECT_TRUE(decomp[0][2]->isCompoundOperation()); + EXPECT_TRUE(decomp[0][2]->isGlobal(n)); + + EXPECT_EQ(decomp[0][3]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][3]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[0][3]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon))); + + EXPECT_EQ(decomp[0][4]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][4]->getTargets(), ::testing::ElementsAre(1)); + EXPECT_THAT(decomp[0][4]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon))); + + EXPECT_TRUE(decomp[0][5]->isCompoundOperation()); + EXPECT_TRUE(decomp[0][5]->isGlobal(n)); + + EXPECT_EQ(decomp[0][6]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][6]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[0][6]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + + EXPECT_EQ(decomp[0][7]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][7]->getTargets(), ::testing::ElementsAre(1)); + EXPECT_THAT(decomp[0][7]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); +} + +TEST_F(DecomposerTest, TwoQubitsTwoLayers) { + // ┌───────┐ ┌───────┐ + // q_0: ─┤ X ├───■───┤ Z ├─ + // └───────┘ │ └───────┘ + // │ ┌───────┐ + // q_1: ─────────────■───┤ X ├─ + // └───────┘ + + size_t n = 2; + qc::QuantumComputation qc(n); + qc.x(0); + qc.cz(0, 1); + qc.z(0); + qc.x(1); + const auto& sched = scheduler.schedule(qc); + auto decomp = decomposer.decompose(qc.getNqubits(), sched).first; + EXPECT_EQ(decomp.size(), 2); + EXPECT_EQ(decomp[0].size(), 5); + EXPECT_EQ(decomp[1].size(), 8); + + // Layer 1 + EXPECT_EQ(decomp[0][0]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][0]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[0][0]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + + EXPECT_TRUE(decomp[0][1]->isCompoundOperation()); + EXPECT_TRUE(decomp[0][1]->isGlobal(n)); + + EXPECT_EQ(decomp[0][2]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][2]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[0][2]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon))); + + EXPECT_TRUE(decomp[0][3]->isCompoundOperation()); + EXPECT_TRUE(decomp[0][3]->isGlobal(n)); + + EXPECT_EQ(decomp[0][4]->getType(), qc::RZ); + EXPECT_THAT(decomp[0][4]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[0][4]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + + // Layer 2 + + EXPECT_EQ(decomp[1][0]->getType(), qc::RZ); + EXPECT_THAT(decomp[1][0]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[1][0]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + + EXPECT_EQ(decomp[1][1]->getType(), qc::RZ); + EXPECT_THAT(decomp[1][1]->getTargets(), ::testing::ElementsAre(1)); + EXPECT_THAT(decomp[1][1]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + + EXPECT_TRUE(decomp[1][2]->isCompoundOperation()); + EXPECT_TRUE(decomp[1][2]->isGlobal(n)); + + EXPECT_EQ(decomp[1][3]->getType(), qc::RZ); + EXPECT_THAT(decomp[1][3]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[1][3]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(0, epsilon))); + + EXPECT_EQ(decomp[1][4]->getType(), qc::RZ); + EXPECT_THAT(decomp[1][4]->getTargets(), ::testing::ElementsAre(1)); + EXPECT_THAT(decomp[1][4]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI, epsilon))); + + EXPECT_TRUE(decomp[1][5]->isCompoundOperation()); + EXPECT_TRUE(decomp[1][5]->isGlobal(n)); + + EXPECT_EQ(decomp[1][6]->getType(), qc::RZ); + EXPECT_THAT(decomp[1][6]->getTargets(), ::testing::ElementsAre(0)); + EXPECT_THAT(decomp[1][6]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, epsilon))); + + EXPECT_EQ(decomp[1][7]->getType(), qc::RZ); + EXPECT_THAT(decomp[1][7]->getTargets(), ::testing::ElementsAre(1)); + EXPECT_THAT(decomp[1][7]->getParameter(), + ::testing::ElementsAre(::testing::DoubleNear(qc::PI_2, 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..bd53830b6 --- /dev/null +++ b/test/na/zoned/test_theta_opt_scheduler.cpp @@ -0,0 +1,668 @@ +/* + * 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/scheduler/ASAPScheduler.hpp" + +#include +#include +#include +#include +#include + +// +// Created by cpsch on 08.04.2026. +// +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) {} +}; + +constexpr static qc::fp epsilon = std::numeric_limits::epsilon() * 1024; + +TEST_F(ThetaOptTest, GraphTest) { + // Circuit + // ┌───────┐ ┌───────┐ + // q_0: ─┤ X ├───■───┤ Z ├─ + // └───────┘ │ └───────┘ + // │ ┌───────┐ + // q_1: ─────────────■───┤ X ├─ + // └───────┘ + + size_t n = 2; + qc::QuantumComputation qc(n); + qc.x(0); + qc.cz(0, 1); + qc.z(0); + qc.x(1); + + auto schedule = scheduler.schedule(qc); + auto one_qubit_gates = NativeGateDecomposer::transformToU3(schedule.first, n); + auto graph = NativeGateDecomposer::convertCircuitToDAG( + {one_qubit_gates, schedule.second}, n); + EXPECT_EQ( + std::get(graph.getNodeValue(0)).qubit, 0); + EXPECT_THAT( + std::get(graph.getNodeValue(0)).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.front().front().angles.at(0), + epsilon), + ::testing::DoubleNear(one_qubit_gates.front().front().angles.at(1), + epsilon), + ::testing::DoubleNear(one_qubit_gates.front().front().angles.at(2), + epsilon))); + EXPECT_THAT(graph.getAdjacent(0), + ::testing::ElementsAre(std::pair(1, 1.0))); + + auto tqg = std::get>(graph.getNodeValue(1)); + EXPECT_THAT(tqg, ::testing::ElementsAre(static_cast(0), + static_cast(1))); + EXPECT_THAT(graph.getAdjacent(1), + ::testing::ElementsAre(std::pair(2, 1.0), + std::pair(3, 1.0))); + + EXPECT_EQ( + std::get(graph.getNodeValue(2)).qubit, 0); + EXPECT_THAT( + std::get(graph.getNodeValue(2)).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.at(1).front().angles.at(0), + epsilon), + ::testing::DoubleNear(one_qubit_gates.at(1).front().angles.at(1), + epsilon), + ::testing::DoubleNear(one_qubit_gates.at(1).front().angles.at(2), + epsilon))); + EXPECT_THAT(graph.getAdjacent(2), ::testing::IsEmpty()); + + EXPECT_EQ( + std::get(graph.getNodeValue(3)).qubit, 1); + EXPECT_THAT( + std::get(graph.getNodeValue(3)).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.at(1).at(1).angles.at(0), + epsilon), + ::testing::DoubleNear(one_qubit_gates.at(1).at(1).angles.at(1), + epsilon), + ::testing::DoubleNear(one_qubit_gates.at(1).at(1).angles.at(2), + epsilon))); + EXPECT_THAT(graph.getAdjacent(3), ::testing::IsEmpty()); +} + +TEST_F(ThetaOptTest, SiftTest) { + // Circuit + // ┌───────┐ ┌───────┐ ┌───────┐ + // q_0: ──┤ X ├───■───────┤ Z ├───■───┤ Y ├─ + // └───────┘ │ └───────┘ │ └───────┘ + // │ ┌───────┐ │ + // q_1: ──────────────■───■───┤ X ├───│───────────── + // │ └───────┘ │ + // ┌───────┐ │ ┌───────┐ │ + // q_1: ──┤ X ├───────■───┤ Y ├───■───────────── + // └───────┘ └───────┘ + + size_t n = 3; + qc::QuantumComputation qc(n); + 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); + + auto schedule = scheduler.schedule(qc); + auto one_qubit_gates = NativeGateDecomposer::transformToU3(schedule.first, n); + auto graph = NativeGateDecomposer::convertCircuitToDAG( + {one_qubit_gates, schedule.second}, n); + + // Perform Sift (multiple times???) + std::vector v_start = {0}; + for (std::size_t i = 0; i < graph.size(); ++i) { + v_start.push_back(i); + } + std::array, 3> v = + NativeGateDecomposer::sift(graph, v_start, n); + + EXPECT_THAT(v[0], ::testing::IsEmpty()); + EXPECT_THAT(v[1], ::testing::ElementsAre(0, 1)); + EXPECT_THAT(v[2], ::testing::ElementsAre(2, 3, 4, 5, 6, 7, 8)); + + v = NativeGateDecomposer::sift(graph, v[2], n); + + EXPECT_THAT(v[0], ::testing::ElementsAre(2, 4)); + EXPECT_THAT(v[1], ::testing::ElementsAre(3, 5, 6)); + EXPECT_THAT(v[2], ::testing::ElementsAre(7, 8)); + + v = NativeGateDecomposer::sift(graph, v[2], n); + + EXPECT_THAT(v[0], ::testing::ElementsAre(7)); + EXPECT_THAT(v[1], ::testing::ElementsAre(8)); + EXPECT_THAT(v[2], ::testing::IsEmpty()); +} + +TEST_F(ThetaOptTest, NextMomentsPushTest) { + // 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) ├────■──── + // └───────────────────┘ └────────────────┘ + 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); + auto schedule = scheduler.schedule(qc); + auto one_qubit_gates = NativeGateDecomposer::transformToU3(schedule.first, n); + auto graph = NativeGateDecomposer::convertCircuitToDAG( + {one_qubit_gates, schedule.second}, n); + auto v = NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6, 7, 8}, n); + NativeGateDecomposer::DiGraph< + std::pair, std::vector>> + subproblem_graph{}; + subproblem_graph.add_Node( + std::pair, std::vector>({}, {})); + auto v_new = NativeGateDecomposer::sift(graph, v[2], n); + auto moments = + NativeGateDecomposer::getPossibleMoments(graph, v[1], v_new, false); + + EXPECT_EQ(moments.size(), 2); + + EXPECT_THAT(moments[0].second, ::testing::DoubleNear(qc::PI, epsilon)); + EXPECT_THAT(moments[0].first[0], ::testing::UnorderedElementsAre(0, 1)); + EXPECT_THAT(moments[0].first[1], ::testing::UnorderedElementsAre(2, 4)); + EXPECT_THAT(moments[0].first[2], ::testing::UnorderedElementsAre(3, 5, 6)); + EXPECT_THAT(moments[0].first[3], ::testing::UnorderedElementsAre(7)); + + EXPECT_THAT(moments[1].second, ::testing::DoubleNear(qc::PI_4, epsilon)); + EXPECT_THAT(moments[1].first[0], ::testing::UnorderedElementsAre(1)); + EXPECT_THAT(moments[1].first[1], ::testing::UnorderedElementsAre(2)); + EXPECT_THAT(moments[1].first[2], ::testing::UnorderedElementsAre(3, 0)); + EXPECT_THAT(moments[1].first[3], ::testing::UnorderedElementsAre(4, 5, 6, 7)); +} + +TEST_F(ThetaOptTest, NextMomentsCond2Test) { + // 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) ├────■──── + // └───────────────────┘ └────────────────┘ + 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(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); + auto schedule = scheduler.schedule(qc); + auto one_qubit_gates = NativeGateDecomposer::transformToU3(schedule.first, n); + auto graph = NativeGateDecomposer::convertCircuitToDAG( + {one_qubit_gates, schedule.second}, n); + auto v = NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6, 7, 8}, n); + NativeGateDecomposer::DiGraph< + std::pair, std::vector>> + subproblem_graph{}; + subproblem_graph.add_Node( + std::pair, std::vector>({}, {})); + auto v_new = NativeGateDecomposer::sift(graph, v[2], n); + auto moments = + NativeGateDecomposer::getPossibleMoments(graph, v[1], v_new, false); + + EXPECT_EQ(moments.size(), 1); + + EXPECT_THAT(moments[0].second, ::testing::DoubleNear(qc::PI, epsilon)); + EXPECT_THAT(moments[0].first[0], ::testing::UnorderedElementsAre(0, 1)); + EXPECT_THAT(moments[0].first[1], ::testing::UnorderedElementsAre(2, 4)); + EXPECT_THAT(moments[0].first[2], ::testing::UnorderedElementsAre(3, 5, 6)); + EXPECT_THAT(moments[0].first[3], ::testing::UnorderedElementsAre(7)); +} + +TEST_F(ThetaOptTest, NextMomentsCond3Test) { + // Circuit + // ┌──────────────────┐ ┌───────┐ + // q_0: ──┤ U(-PI,Pi/2,PI/4) ├─────────■───┤ X ├─────■──── + // └──────────────────┘ │ └───────┘ │ + // │ ┌───────┐ │ + // q_1: ──────────────────────────■────■───┤ Y ├─────│──── + // │ └───────┘ │ + // ┌───────────────────┐ │ │ + // q_2: ──┤ U(PI/4,PI/4,PI/4) ├───■──────────────────────■──── + // └───────────────────┘ + 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.cz(0, 2); + auto schedule = scheduler.schedule(qc); + auto one_qubit_gates = NativeGateDecomposer::transformToU3(schedule.first, n); + auto graph = NativeGateDecomposer::convertCircuitToDAG( + {one_qubit_gates, schedule.second}, n); + auto v = NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6, 7, 8}, n); + NativeGateDecomposer::DiGraph< + std::pair, std::vector>> + subproblem_graph{}; + subproblem_graph.add_Node( + std::pair, std::vector>({}, {})); + auto v_new = NativeGateDecomposer::sift(graph, v[2], n); + auto moments = + NativeGateDecomposer::getPossibleMoments(graph, v[1], v_new, false); + + EXPECT_EQ(moments.size(), 1); + + EXPECT_THAT(moments[0].second, ::testing::DoubleNear(qc::PI, epsilon)); + EXPECT_THAT(moments[0].first[0], ::testing::UnorderedElementsAre(0, 1)); + EXPECT_THAT(moments[0].first[1], ::testing::UnorderedElementsAre(2, 3)); + EXPECT_THAT(moments[0].first[2], ::testing::UnorderedElementsAre(4, 5)); + EXPECT_THAT(moments[0].first[3], ::testing::UnorderedElementsAre(6)); +} + +TEST_F(ThetaOptTest, RecursionBaseTest) { + // 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); + + auto schedule = scheduler.schedule(qc); + auto one_qubit_gates = NativeGateDecomposer::transformToU3(schedule.first, n); + auto graph = NativeGateDecomposer::convertCircuitToDAG( + {one_qubit_gates, schedule.second}, n); + auto v = NativeGateDecomposer::sift(graph, {0, 1, 2, 3, 4, 5, 6, 7, 8}, n); + NativeGateDecomposer::DiGraph< + std::pair, std::vector>> + subproblem_graph{}; + subproblem_graph.add_Node( + std::pair, std::vector>({}, {})); + std::map>> memo = + std::map>>(); + auto result = NativeGateDecomposer::scheduleRemaining( + v, graph, subproblem_graph, 0, n, false, memo); + + EXPECT_EQ(result, 5 * qc::PI_2); + + EXPECT_EQ(subproblem_graph.size(), 1 + 6); + auto t = subproblem_graph.getAdjacent(0); + EXPECT_THAT(subproblem_graph.getAdjacent(0), + ::testing::UnorderedElementsAre(::testing::Pair(1, qc::PI), + ::testing::Pair(4, qc::PI_4))); + + EXPECT_THAT(subproblem_graph.getNodeValue(1).first, ::testing::IsEmpty()); + EXPECT_THAT(subproblem_graph.getNodeValue(1).second, + ::testing::UnorderedElementsAre(0, 1)); + EXPECT_THAT(subproblem_graph.getAdjacent(1), + ::testing::UnorderedElementsAre(::testing::Pair(2, qc::PI))); + EXPECT_THAT(subproblem_graph.getNodeValue(2).first, + ::testing::UnorderedElementsAre(2, 4)); + EXPECT_THAT(subproblem_graph.getNodeValue(2).second, + ::testing::UnorderedElementsAre(3, 5, 6)); + EXPECT_THAT(subproblem_graph.getAdjacent(2), + ::testing::UnorderedElementsAre(::testing::Pair(3, qc::PI_2))); + EXPECT_THAT(subproblem_graph.getNodeValue(3).first, + ::testing::UnorderedElementsAre(7)); + EXPECT_THAT(subproblem_graph.getNodeValue(3).second, + ::testing::UnorderedElementsAre(8)); + EXPECT_THAT(subproblem_graph.getAdjacent(3), ::testing::IsEmpty()); + + EXPECT_THAT(subproblem_graph.getNodeValue(4).first, ::testing::IsEmpty()); + EXPECT_THAT(subproblem_graph.getNodeValue(4).second, + ::testing::UnorderedElementsAre(1)); + EXPECT_THAT(subproblem_graph.getAdjacent(4), + ::testing::UnorderedElementsAre(::testing::Pair(5, qc::PI))); + EXPECT_THAT(subproblem_graph.getNodeValue(5).first, + ::testing::UnorderedElementsAre(2)); + EXPECT_THAT(subproblem_graph.getNodeValue(5).second, + ::testing::UnorderedElementsAre(0, 3)); + EXPECT_THAT(subproblem_graph.getAdjacent(5), + ::testing::UnorderedElementsAre(::testing::Pair(6, qc::PI))); + EXPECT_THAT(subproblem_graph.getNodeValue(6).first, + ::testing::UnorderedElementsAre(4)); + EXPECT_THAT(subproblem_graph.getNodeValue(6).second, + ::testing::UnorderedElementsAre(5, 6)); + EXPECT_THAT(subproblem_graph.getAdjacent(6), + ::testing::UnorderedElementsAre(::testing::Pair(3, qc::PI_2))); +} + +TEST_F(ThetaOptTest, CheapestPathTest) { + // Subproblem graph! + NativeGateDecomposer::DiGraph< + std::pair, std::vector>> + subproblem_graph{}; + for (int i = 0; i < 14; i++) { + subproblem_graph.add_Node( + std::pair, std::vector>({}, {})); + } + subproblem_graph.addEdge(0, 1); + subproblem_graph.addEdge(0, 2); + subproblem_graph.addEdge(0, 3, 0.5); + subproblem_graph.addEdge(1, 4); + subproblem_graph.addEdge(2, 5); + subproblem_graph.addEdge(3, 6); + subproblem_graph.addEdge(3, 7); + subproblem_graph.addEdge(4, 8); + subproblem_graph.addEdge(5, 8); + subproblem_graph.addEdge(5, 9); + subproblem_graph.addEdge(6, 10); + subproblem_graph.addEdge(7, 11); + subproblem_graph.addEdge(8, 12); + subproblem_graph.addEdge(11, 13); + auto leaf_nodes = NativeGateDecomposer::findLeafNodes(subproblem_graph); + auto path = + NativeGateDecomposer::findCheapestPath(subproblem_graph, leaf_nodes); + EXPECT_THAT(leaf_nodes, ::testing::ElementsAre(9, 10, 12, 13)); + EXPECT_THAT(path, ::testing::ElementsAre(3, 6, 10)); +} + +TEST_F(ThetaOptTest, BuildScheduleTest) { + // Circuit + // ┌───────┐ ┌───────┐ ┌───────┐ + // q_0: ──┤ X ├───■───────┤ Z ├───■───┤ Y ├─ + // └───────┘ │ └───────┘ │ └───────┘ + // │ ┌───────┐ │ + // q_1: ──────────────■───■───┤ X ├───│───────────── + // │ └───────┘ │ + // ┌───────┐ │ ┌───────┐ │ + // q_2: ──┤ X ├───────■───┤ Y ├───■───────────── + // └───────┘ └───────┘ + + size_t n = 3; + qc::QuantumComputation qc(n); + 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); + + auto asap_schedule = scheduler.schedule(qc); + auto one_qubit_gates = + NativeGateDecomposer::transformToU3(asap_schedule.first, n); + auto graph = NativeGateDecomposer::convertCircuitToDAG( + {one_qubit_gates, asap_schedule.second}, n); + + // Create Basic Subproblem graph from purely sifted schedule + NativeGateDecomposer::DiGraph< + std::pair, std::vector>> + subproblem_graph{}; + subproblem_graph.add_Node( + std::pair, std::vector>({}, {})); + subproblem_graph.add_Node( + std::pair, std::vector>({}, + {0, 1})); + subproblem_graph.add_Node( + std::pair, std::vector>({2, 4}, + {3, 5, 6})); + subproblem_graph.add_Node( + std::pair, std::vector>({7}, {8})); + subproblem_graph.addEdge(0, 1, NativeGateDecomposer::maxTheta(graph, {0, 1})); + subproblem_graph.addEdge(1, 2, + NativeGateDecomposer::maxTheta(graph, {3, 5, 6})); + subproblem_graph.addEdge(2, 3, NativeGateDecomposer::maxTheta(graph, {8})); + + auto schedule = NativeGateDecomposer::buildSchedule(graph, subproblem_graph); + + EXPECT_EQ(schedule.first.size(), 4); + EXPECT_EQ(schedule.second.size(), 3); + + EXPECT_EQ(schedule.first.front().at(0).qubit, 0); + EXPECT_THAT(schedule.first.front().at(0).angles, + ::testing::ElementsAre(one_qubit_gates.at(0).at(0).angles[0], + one_qubit_gates.at(0).at(0).angles[1], + one_qubit_gates.at(0).at(0).angles[2])); + EXPECT_EQ(schedule.first.at(0).at(1).qubit, 2); + EXPECT_THAT(schedule.first.at(0).at(1).angles, + ::testing::ElementsAre(one_qubit_gates.at(0).at(0).angles[0], + one_qubit_gates.at(0).at(0).angles[1], + one_qubit_gates.at(0).at(0).angles[2])); + + EXPECT_THAT(schedule.second.at(0).at(0), ::testing::ElementsAre(0, 1)); + + EXPECT_TRUE(schedule.first.at(1).empty()); + + EXPECT_THAT(schedule.second.at(1).at(0), ::testing::ElementsAre(1, 2)); + + EXPECT_EQ(schedule.first.at(2).at(0).qubit, 0); + EXPECT_THAT(schedule.first.at(2).at(0).angles, + ::testing::ElementsAre(one_qubit_gates.at(1).at(0).angles[0], + one_qubit_gates.at(1).at(0).angles[1], + one_qubit_gates.at(1).at(0).angles[2])); + // Two gates flipped?? + EXPECT_EQ(schedule.first.at(2).at(1).qubit, 1); + EXPECT_THAT(schedule.first.at(2).at(1).angles, + ::testing::ElementsAre(one_qubit_gates.at(2).at(0).angles[0], + one_qubit_gates.at(2).at(0).angles[1], + one_qubit_gates.at(2).at(0).angles[2])); + EXPECT_EQ(schedule.first.at(2).at(2).qubit, 2); + EXPECT_THAT(schedule.first.at(2).at(2).angles, + ::testing::ElementsAre(one_qubit_gates.at(2).at(1).angles[0], + one_qubit_gates.at(2).at(1).angles[1], + one_qubit_gates.at(2).at(1).angles[2])); + + EXPECT_THAT(schedule.second.at(2).at(0), ::testing::ElementsAre(0, 2)); + + EXPECT_EQ(schedule.first.at(3).at(0).qubit, 0); + EXPECT_THAT(schedule.first.at(3).at(0).angles, + ::testing::ElementsAre(one_qubit_gates.at(3).at(0).angles[0], + one_qubit_gates.at(3).at(0).angles[1], + one_qubit_gates.at(3).at(0).angles[2])); +} + +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: ─ ─ ────────────────────── + + 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); + + auto schedule = scheduler.schedule(qc); + auto one_qubit_gates = NativeGateDecomposer::transformToU3(schedule.first, n); + auto theta_opt_schedule = + decomposer.scheduleThetaOpt({one_qubit_gates, schedule.second}, n); + + EXPECT_EQ(theta_opt_schedule.first.size(), 4); + EXPECT_EQ(theta_opt_schedule.second.size(), 3); + + EXPECT_EQ(theta_opt_schedule.first.at(0).size(), 2); + EXPECT_EQ(theta_opt_schedule.first.at(1).size(), 0); + EXPECT_EQ(theta_opt_schedule.first.at(2).size(), 3); + EXPECT_EQ(theta_opt_schedule.first.at(3).size(), 1); + + EXPECT_EQ(theta_opt_schedule.second.at(0).size(), 1); + EXPECT_EQ(theta_opt_schedule.second.at(1).size(), 1); + EXPECT_EQ(theta_opt_schedule.second.at(2).size(), 1); + + EXPECT_EQ(theta_opt_schedule.first.at(0).at(0).qubit, 0); + // TODO: DOUBLE Nears!!! + EXPECT_THAT( + theta_opt_schedule.first.at(0).at(0).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.at(0).at(0).angles[0], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(0).at(0).angles[1], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(0).at(0).angles[2], + epsilon))); + EXPECT_EQ(theta_opt_schedule.first.at(0).at(1).qubit, 2); + EXPECT_THAT( + theta_opt_schedule.first.at(0).at(1).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.at(0).at(1).angles[0], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(0).at(1).angles[1], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(0).at(1).angles[2], + epsilon))); + + EXPECT_THAT(theta_opt_schedule.second.at(0).front(), + ::testing::ElementsAre(1, 2)); + + EXPECT_THAT(theta_opt_schedule.second.at(1).front(), + ::testing::ElementsAre(0, 1)); + // QUAT + EXPECT_EQ(theta_opt_schedule.first.at(2).at(0).qubit, 2); + EXPECT_THAT( + theta_opt_schedule.first.at(2).at(0).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.at(1).at(0).angles[0], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(1).at(0).angles[1], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(1).at(0).angles[2], + epsilon))); + EXPECT_EQ(theta_opt_schedule.first.at(2).at(1).qubit, 0); + EXPECT_THAT( + theta_opt_schedule.first.at(2).at(1).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.at(2).at(0).angles[0], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(2).at(0).angles[1], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(2).at(0).angles[2], + epsilon))); + EXPECT_EQ(theta_opt_schedule.first.at(2).at(2).qubit, 1); + EXPECT_THAT( + theta_opt_schedule.first.at(2).at(2).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.at(2).at(1).angles[0], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(2).at(1).angles[1], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(2).at(1).angles[2], + epsilon))); + + EXPECT_THAT(theta_opt_schedule.second.at(2).front(), + ::testing::ElementsAre(0, 2)); + + EXPECT_EQ(theta_opt_schedule.first.at(3).at(0).qubit, 0); + EXPECT_THAT( + theta_opt_schedule.first.at(3).at(0).angles, + ::testing::ElementsAre( + ::testing::DoubleNear(one_qubit_gates.at(3).at(0).angles[0], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(3).at(0).angles[1], epsilon), + ::testing::DoubleNear(one_qubit_gates.at(3).at(0).angles[2], + epsilon))); +} + +TEST_F(ThetaOptTest, CompleteTest) { + 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); + auto schedule = scheduler.schedule(qc); + auto res = decomposer.decompose(4, schedule); + EXPECT_EQ(res.first.size(), 5); +} +} // namespace na::zoned