diff --git a/CHANGELOG.md b/CHANGELOG.md index fab293a18..36fac3c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ - ✨ Enable code generation for relaxed routing constraints ([#848]) ([**@ystade**]) - ✨ Add `max_filling_factor` to scheduler in Zoned Neutral Atom Compiler ([#847]) ([**@ystade**]) +- ✨ Added extension to the hybrid routing mapper to also support Bridge gates, Passby moves and Flying ancillas ([#832]) ([**@lsschmid**]) +- ✨ Added hybrid synthesis routing for iterative circuit constructions ([#832]) ([**@lsschmid**]) ## [3.4.0] - 2025-10-15 @@ -160,6 +162,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ [#848]: https://github.com/munich-quantum-toolkit/qmap/pull/848 [#847]: https://github.com/munich-quantum-toolkit/qmap/pull/847 +[#832]: https://github.com/munich-quantum-toolkit/qmap/pull/832 [#804]: https://github.com/munich-quantum-toolkit/qmap/pull/804 [#803]: https://github.com/munich-quantum-toolkit/qmap/pull/803 [#796]: https://github.com/munich-quantum-toolkit/qmap/pull/796 @@ -192,6 +195,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ [**@burgholzer**]: https://github.com/burgholzer [**@ystade**]: https://github.com/ystade [**@denialhaag**]: https://github.com/denialhaag +[**@lsschmid**]: https://github.com/lsschmid diff --git a/README.md b/README.md index 52d9902fc..a2d008894 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ It builds upon [MQT Core](https://github.com/munich-quantum-toolkit/core), which - Clifford circuit synthesis and optimization: SAT-based depth/gate-optimal Clifford synthesis with optional destabilizer preservation, plus a fast heuristic splitter for larger circuits. [Guide](https://mqt.readthedocs.io/projects/qmap/en/latest/synthesis.html) - Zoned neutral-atom compilers: routing-agnostic and routing-aware flows that place, route, and schedule atom transfers between storage/entanglement zones. [Guide](https://mqt.readthedocs.io/projects/qmap/en/latest/na_zoned_compiler.html) - Neutral-atom logical state preparation (NASP): SMT-based generator for optimal preparation schedules of logical graph states on zoned architectures. [Guide](https://mqt.readthedocs.io/projects/qmap/en/latest/na_state_prep.html) -- Hybrid circuit mapper for neutral atom quantum computers: a hybrid approach combining superconducting mapping techniques with atom shuttling. +- Hybrid circuit mapper for neutral atom quantum computers: a hybrid approach combining superconducting mapping techniques with atom shuttling. [Guide](https://mqt.readthedocs.io/projects/qmap/en/latest/na_hybrid.html) - Python-first API with Qiskit integration: pass `QuantumCircuit` or OpenQASM; one-call `compile()` or `optimize_clifford()` via plugin wrappers. [API](https://mqt.readthedocs.io/projects/qmap/en/latest/api/mqt/qmap/index.html) - Efficient and portable: C++20 core with Z3-backed solvers, prebuilt wheels for Linux/macOS/Windows via [PyPI](https://pypi.org/project/mqt.qmap/). diff --git a/UPGRADING.md b/UPGRADING.md index 766757259..1daff2c89 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -12,6 +12,14 @@ The code generator of the zoned neutral atom compiler is updated to also handle In contrast to the strict routing, a relaxed routing can change the relative order of atoms. The constraint that remains is that atoms previously in one row (column) must remain in the same row (column) after the routing. +Additionally, we also introduce an extension to the Hybrid Neutral Atom Mapper (HyRoNA), which unifies gate-based routing (SWAP/BRIDGE) with atom shuttling, pass-by, and an optional flying ancilla to find the most suitable routing. + +Existing workflows should continue to function. +The optionally new parameters are `usePassBy=False`, `numFlyingAncillas=0`, and `maxBridgeDistance=0` which can all be disabled with the above values to recover the previous behavior. +Enabling/increasing the corresponding parameters allows enabling individually single routing strategies. + +The hybrid mapper now also optionally yields a `.naviz` output which can be handled similarly to the zoned architecture compiler. + ## [3.4.0] ### End of support for Python 3.9 diff --git a/bindings/hybrid_mapper/hybrid_mapper.cpp b/bindings/hybrid_mapper/hybrid_mapper.cpp index 51335c7ca..baedf2407 100644 --- a/bindings/hybrid_mapper/hybrid_mapper.cpp +++ b/bindings/hybrid_mapper/hybrid_mapper.cpp @@ -9,11 +9,13 @@ */ #include "hybridmap/HybridNeutralAtomMapper.hpp" +#include "hybridmap/HybridSynthesisMapper.hpp" #include "hybridmap/NeutralAtomArchitecture.hpp" #include "hybridmap/NeutralAtomScheduler.hpp" #include "hybridmap/NeutralAtomUtils.hpp" #include "qasm3/Importer.hpp" +#include #include #include #include @@ -23,6 +25,7 @@ #include #include // NOLINT(misc-include-cleaner) #include +#include namespace py = pybind11; using namespace pybind11::literals; @@ -42,154 +45,166 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { m, "InitialCircuitMapping", "enum.Enum", "Initial mapping between circuit qubits and hardware qubits.") .value("identity", na::InitialMapping::Identity, "Identity mapping.") + .value("graph", na::InitialMapping::Graph, "Graph matching mapping.") .export_values() .finalize(); py::class_( - m, "HybridMapperParameters", - "Parameters for the Neutral Atom Hybrid Mapper.") - .def(py::init<>()) - .def(py::init(), - "lookahead_weight_swaps"_a = 0.1, "lookahead_weight_moves"_a = 0.1, - "decay"_a = 0.1, "shuttling_time_weight"_a = 1, "gate_weight"_a = 1, - "shuttling_weight"_a = 1) + m, "MapperParameters", "Parameters controlling the mapper behavior.") + .def(py::init<>(), + "Create a MapperParameters instance with default values.") + .def_readwrite("lookahead_depth", &na::MapperParameters::lookaheadDepth, + "Depth of lookahead for mapping decisions.") .def_readwrite("lookahead_weight_swaps", &na::MapperParameters::lookaheadWeightSwaps, - "Weight for the lookahead for the SWAP gates. 0 means no " - "lookahead is considered.") + "Weight assigned to swap operations during lookahead.") .def_readwrite("lookahead_weight_moves", &na::MapperParameters::lookaheadWeightMoves, - "Weight for the lookahead for the MOVE gates. 0 means no " - "lookahead is considered.") + "Weight assigned to move operations during lookahead.") .def_readwrite("decay", &na::MapperParameters::decay, - "Decay factor for the blocking constraint to avoid SWAPs " - "that block each other. 0 means no decay.") - .def_readwrite( - "shuttling_time_weight", &na::MapperParameters::shuttlingTimeWeight, - "Weight how much the shuttling Times should be considered.") + "Decay factor for gate blocking.") + .def_readwrite("shuttling_time_weight", + &na::MapperParameters::shuttlingTimeWeight, + "Weight for shuttling time in cost evaluation.") .def_readwrite( - "gate_weight", &na::MapperParameters::gateWeight, - "Weight for the SWAP gates. Higher means mapper will prefer SWAP " - "gates over shuttling. 0 means only shuttling is used.") - .def_readwrite( - "shuttling_weight", &na::MapperParameters::shuttlingWeight, - "Weight for the shuttling. Higher means mapper will prefer " - "shuttling over SWAP gates. 0 means only SWAP gates are " - "used.") + "dynamic_mapping_weight", &na::MapperParameters::dynamicMappingWeight, + "Weight for dynamic remapping (SWAPs or MOVEs) in cost evaluation.") + .def_readwrite("gate_weight", &na::MapperParameters::gateWeight, + "Weight for gate execution in cost evaluation.") + .def_readwrite("shuttling_weight", &na::MapperParameters::shuttlingWeight, + "Weight for shuttling operations in cost evaluation.") .def_readwrite( "seed", &na::MapperParameters::seed, - "Seed for the random number generator. 0 means random seed.") + "Random seed for stochastic decisions (initial mapping, etc.).") + .def_readwrite("num_flying_ancillas", + &na::MapperParameters::numFlyingAncillas, + "Number of ancilla qubits to be used (0 or 1 for now).") + .def_readwrite("limit_shuttling_layer", + &na::MapperParameters::limitShuttlingLayer, + "Maximum allowed shuttling layer (default: 10).") + .def_readwrite("max_bridge_distance", + &na::MapperParameters::maxBridgeDistance, + "Maximum distance for bridge operations.") + .def_readwrite("use_pass_by", &na::MapperParameters::usePassBy, + "Enable or disable pass-by operations.") .def_readwrite("verbose", &na::MapperParameters::verbose, - "Print additional information during the mapping process.") - .def_readwrite("initial_mapping", &na::MapperParameters::initialMapping, - "Initial mapping between circuit qubits and hardware " - "qubits."); + "Enable verbose logging for debugging.") + .def_readwrite("initial_coord_mapping", + &na::MapperParameters::initialCoordMapping, + "Strategy for initial coordinate mapping."); + + py::class_(m, "MapperStats") + .def(py::init<>()) + .def_readwrite("num_swaps", &na::MapperStats::nSwaps, + "Number of swap operations performed.") + .def_readwrite("num_bridges", &na::MapperStats::nBridges, + "Number of bridge operations performed.") + .def_readwrite("num_f_ancillas", &na::MapperStats::nFAncillas, + "Number of fresh ancilla qubits used.") + .def_readwrite("num_moves", &na::MapperStats::nMoves, + "Number of move operations performed.") + .def_readwrite("num_pass_by", &na::MapperStats::nPassBy, + "Number of pass-by operations performed."); py::class_(m, "NeutralAtomHybridArchitecture") .def(py::init(), "filename"_a) .def("load_json", &na::NeutralAtomArchitecture::loadJson, "json_filename"_a) .def_readwrite("name", &na::NeutralAtomArchitecture::name, - "Name of the " - "architecture") + "Name of the architecture.") .def_property_readonly( - "nrows", &na::NeutralAtomArchitecture::getNrows, - "Number of rows in a rectangular grid SLM arrangement") + "num_rows", &na::NeutralAtomArchitecture::getNrows, + "Number of rows in a rectangular grid SLM arrangement.") .def_property_readonly( - "ncolumns", &na::NeutralAtomArchitecture::getNcolumns, - "Number of columns in a rectangular grid SLM arrangement") + "num_columns", &na::NeutralAtomArchitecture::getNcolumns, + "Number of columns in a rectangular grid SLM arrangement.") .def_property_readonly( - "npositions", &na::NeutralAtomArchitecture::getNpositions, - "Total number of positions in a rectangular grid SLM arrangement") + "num_positions", &na::NeutralAtomArchitecture::getNpositions, + "Total number of positions in a rectangular grid SLM arrangement.") .def_property_readonly( - "naods", &na::NeutralAtomArchitecture::getNAods, - "Number of independent 2D acousto-optic deflectors") - .def_property_readonly("naod_coordinates", - &na::NeutralAtomArchitecture::getNAodCoordinates, - "Maximal number of AOD rows/columns (NOT USED)") - .def_property_readonly("nqubits", + "num_aods", &na::NeutralAtomArchitecture::getNAods, + "Number of independent 2D acousto-optic deflectors.") + .def_property_readonly("num_qubits", &na::NeutralAtomArchitecture::getNqubits, "Number of atoms in the neutral atom quantum " - "computer that can be used as qubits") + "computer that can be used as qubits.") .def_property_readonly( "inter_qubit_distance", &na::NeutralAtomArchitecture::getInterQubitDistance, - "Distance " - "between " - "SLM traps in " - "micrometers") + "Distance between SLM traps in micrometers.") .def_property_readonly("interaction_radius", &na::NeutralAtomArchitecture::getInteractionRadius, - "Interaction radius in inter-qubit distances") + "Interaction radius in inter-qubit distances.") .def_property_readonly("blocking_factor", &na::NeutralAtomArchitecture::getBlockingFactor, - "Blocking factor for parallel Rydberg gates") + "Blocking factor for parallel Rydberg gates.") .def_property_readonly( "naod_intermediate_levels", &na::NeutralAtomArchitecture::getNAodIntermediateLevels, - "Number of possible AOD positions between two SLM traps") + "Number of possible AOD positions between two SLM traps.") .def_property_readonly("decoherence_time", &na::NeutralAtomArchitecture::getDecoherenceTime, - "Decoherence time in microseconds") + "Decoherence time in microseconds.") .def("compute_swap_distance", static_cast( &na::NeutralAtomArchitecture::getSwapDistance), - "Number of SWAP gates required between two positions", + "Number of SWAP gates required between two positions.", py::arg("idx1"), py::arg("idx2")) .def("get_gate_time", &na::NeutralAtomArchitecture::getGateTime, - "Execution time of certain gate in microseconds", "s"_a) + "Execution time of certain gate in microseconds.", "s"_a) .def("get_gate_average_fidelity", &na::NeutralAtomArchitecture::getGateAverageFidelity, - "Average gate fidelity from [0,1]", "s"_a) + "Average gate fidelity from [0,1].", "s"_a) .def("get_nearby_coordinates", &na::NeutralAtomArchitecture::getNearbyCoordinates, "Positions that are within the interaction radius of the passed " - "position", - "idx"_a) - .def("get_animation_csv", &na::NeutralAtomArchitecture::getAnimationCsv, - "Returns string representation of the architecture used for " - "animation") - .def("save_animation_csv", &na::NeutralAtomArchitecture::saveAnimationCsv, - "filename"_a, "Saves the animation csv string to a file"); + "position.", + "idx"_a); py::class_( m, "HybridNAMapper", "Neutral Atom Hybrid Mapper that can use both SWAP gates and AOD " - "movements to map a quantum circuit to a neutral atom quantum computer.") - .def(py::init(), - "Create Hybrid NA Mapper with mapper parameters", - py::keep_alive<1, 2>(), py::keep_alive<1, 3>(), "arch"_a, - "params"_a = na::MapperParameters()) + "movements to map a quantum circuit to a neutral atom quantum " + "computer.") + .def( + py::init(), + "Create Hybrid NA Mapper with mapper parameters.", + py::keep_alive<1, 2>(), py::keep_alive<1, 3>(), "arch"_a, "params"_a) .def("set_parameters", &na::NeutralAtomMapper::setParameters, - "Set the parameters for the Hybrid NA Mapper", "params"_a) + "Set the parameters for the Hybrid NA Mapper.", "params"_a, + py::keep_alive<1, 2>()) + .def("get_init_hw_pos", &na::NeutralAtomMapper::getInitHwPos, + "Get the initial hardware positions, required to create an " + "animation.") .def( - "get_init_hw_pos", &na::NeutralAtomMapper::getInitHwPos, - "Get the initial hardware positions, required to create an animation") - .def("map", &na::NeutralAtomMapper::mapAndConvert, - "Map a quantum circuit to the neutral atom quantum computer", - "circ"_a, "initial_mapping"_a = na::InitialMapping::Identity, - "verbose"_a = false) + "map", + [](na::NeutralAtomMapper& mapper, qc::QuantumComputation& circ, + const na::InitialMapping initialMapping) { + mapper.map(circ, initialMapping); + }, + "Map a quantum circuit object to the neutral atom quantum computer.", + "qc"_a, "initial_mapping"_a = na::InitialMapping::Identity) .def( "map_qasm_file", [](na::NeutralAtomMapper& mapper, const std::string& filename, const na::InitialMapping initialMapping) { - auto qc = qasm3::Importer::importf(filename); - mapper.map(qc, initialMapping); + auto circ = qasm3::Importer::importf(filename); + mapper.map(circ, initialMapping); }, - "Map a quantum circuit to the neutral atom quantum computer", + "Map a quantum circuit to the neutral atom quantum computer.", "filename"_a, "initial_mapping"_a = na::InitialMapping::Identity) - .def("get_mapped_qc", &na::NeutralAtomMapper::getMappedQc, - "Returns the mapped circuit as an extended qasm2 string") - .def("save_mapped_qc", &na::NeutralAtomMapper::saveMappedQc, - "Saves the mapped circuit as an extended qasm2 string to a file", - "filename"_a) - .def("get_mapped_qc_aod", &na::NeutralAtomMapper::getMappedQcAOD, - "Returns the mapped circuit as an extended qasm2 string with native " - "AOD movements") - .def("save_mapped_qc_aod", &na::NeutralAtomMapper::saveMappedQcAOD, - "Saves the mapped circuit as an extended qasm2 string with native " - "AOD movements to a file", + .def("get_stats", &na::NeutralAtomMapper::getStatsMap, + "Returns the statistics of the mapping.") + .def("get_mapped_qc_qasm", &na::NeutralAtomMapper::getMappedQcQasm, + "Returns the mapped circuit as an extended qasm2 string.") + .def("get_mapped_qc_aod_qasm", &na::NeutralAtomMapper::getMappedQcAodQasm, + "Returns the mapped circuit with AOD operations as an extended " + "qasm2 string.") + .def("save_mapped_qc_aod_qasm", + &na::NeutralAtomMapper::saveMappedQcAodQasm, + "Saves the mapped circuit with AOD operations as an extended qasm2 " + "string.", "filename"_a) .def( "schedule", @@ -199,10 +214,110 @@ PYBIND11_MODULE(MQT_QMAP_MODULE_NAME, m, py::mod_gil_not_used()) { shuttlingSpeedFactor); return results.toMap(); }, - "Schedule the mapped circuit", "verbose"_a = false, + "Schedule the mapped circuit.", "verbose"_a = false, "create_animation_csv"_a = false, "shuttling_speed_factor"_a = 1.0) - .def("get_animation_csv", &na::NeutralAtomMapper::getAnimationCsv, - "Returns the animation csv string") - .def("save_animation_csv", &na::NeutralAtomMapper::saveAnimationCsv, - "Saves the animation csv string to a file", "filename"_a); + .def("save_animation_files", &na::NeutralAtomMapper::saveAnimationFiles, + "Saves the animation files (.naviz and .namachine) for the " + "scheduling.", + "filename"_a) + .def("get_animation_viz", &na::NeutralAtomMapper::getAnimationViz, + "Returns the .naviz event-log content for the last scheduling."); + + py::class_( + m, "HybridSynthesisMapper", + "Neutral Atom Mapper that can evaluate different synthesis steps " + "to choose the best one.") + .def(py::init(), + "Create Hybrid Synthesis Mapper with mapper parameters.", + py::keep_alive<1, 2>(), py::keep_alive<1, 3>(), "arch"_a, + "params"_a = na::MapperParameters()) + .def("set_parameters", &na::HybridSynthesisMapper::setParameters, + "Set the parameters for the Hybrid Synthesis Mapper.", "params"_a, + py::keep_alive<1, 2>()) + .def("init_mapping", &na::HybridSynthesisMapper::initMapping, + "Initializes the synthesized and mapped circuits and mapping " + "structures for the given number of qubits.", + "n_qubits"_a) + .def("get_mapped_qc_qasm", &na::HybridSynthesisMapper::getMappedQcQasm, + "Returns the mapped circuit as an extended qasm2 string.") + .def("save_mapped_qc_qasm", &na::HybridSynthesisMapper::saveMappedQcQasm, + "Saves the mapped circuit as an extended qasm2 to a file.", + "filename"_a) + .def("convert_to_aod", &na::HybridSynthesisMapper::convertToAod, + "Converts the mapped circuit to " + "native AOD movements.") + .def( + "get_mapped_qc_aod_qasm", + &na::HybridSynthesisMapper::getMappedQcAodQasm, + "Returns the mapped circuit with native AOD movements as an extended " + "qasm2 string.") + .def("save_mapped_qc_aod_qasm", + &na::HybridSynthesisMapper::saveMappedQcAodQasm, + "Saves the mapped circuit with native AOD movements as an extended " + "qasm2 to a " + "file.", + "filename"_a) + .def("get_synthesized_qc_qasm", + &na::HybridSynthesisMapper::getSynthesizedQcQASM, + "Returns the synthesized circuit with all gates but not " + "mapped to the hardware as a qasm2 string.") + .def("save_synthesized_qc_qasm", + &na::HybridSynthesisMapper::saveSynthesizedQc, + "Saves the synthesized circuit with all gates but not " + "mapped to the hardware as qasm2 to a file.", + "filename"_a) + .def( + "append_without_mapping", + [](na::HybridSynthesisMapper& mapper, qc::QuantumComputation& qc) { + mapper.appendWithoutMapping(qc); + }, + "Appends the given QuantumComputation to the synthesized " + "QuantumComputation without mapping it to the hardware.", + "qc"_a) + .def( + "append_with_mapping", + [](na::HybridSynthesisMapper& mapper, qc::QuantumComputation& qc) { + mapper.appendWithMapping(qc); + }, + "Appends the given QuantumComputation to the synthesized " + "QuantumComputation and maps the gates to the hardware.", + "qc"_a) + .def( + "get_circuit_adjacency_matrix", + [](const na::HybridSynthesisMapper& mapper) { + const auto symAdjMatrix = mapper.getCircuitAdjacencyMatrix(); + const auto n = symAdjMatrix.size(); + std::vector> adjMatrix(n, std::vector(n)); + for (size_t i = 0; i < n; ++i) { + for (size_t j = 0; j < n; ++j) { + adjMatrix[i][j] = symAdjMatrix(i, j); + } + } + return adjMatrix; + }, + "Returns the current circuit-qubit adjacency matrix used for " + "mapping.") + .def( + "evaluate_synthesis_steps", + [](na::HybridSynthesisMapper& mapper, + std::vector& qcs, bool alsoMap) { + return mapper.evaluateSynthesisSteps(qcs, alsoMap); + }, + "Evaluates the synthesis steps proposed by the ZX extraction. " + "Returns a list of fidelities of the mapped synthesis steps.", + "synthesis_steps"_a, "also_map"_a = false) + .def("complete_remap", &na::HybridSynthesisMapper::completeRemap, + "Remaps the synthesized QuantumComputation to the hardware.", + "initial_mapping"_a = na::InitialMapping::Identity) + .def( + "schedule", + [](na::HybridSynthesisMapper& mapper, const bool verbose, + const bool createAnimationCsv, const double shuttlingSpeedFactor) { + const auto results = mapper.schedule(verbose, createAnimationCsv, + shuttlingSpeedFactor); + return results.toMap(); + }, + "Schedule the mapped circuit.", "verbose"_a = false, + "create_animation_csv"_a = false, "shuttling_speed_factor"_a = 1.0); } diff --git a/docs/images/hybrid_shuttling.gif b/docs/images/hybrid_shuttling.gif new file mode 100644 index 000000000..2bd43604a Binary files /dev/null and b/docs/images/hybrid_shuttling.gif differ diff --git a/docs/index.md b/docs/index.md index f76396076..c660878bd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,6 +44,7 @@ mapping synthesis na_state_prep na_zoned_compiler +na_hybrid references CHANGELOG UPGRADING diff --git a/docs/na_hybrid.md b/docs/na_hybrid.md new file mode 100644 index 000000000..f454ec378 --- /dev/null +++ b/docs/na_hybrid.md @@ -0,0 +1,287 @@ +--- +file_format: mystnb +kernelspec: + name: python3 + number_source_lines: true +--- + +```{code-cell} ipython3 +:tags: [remove-cell] +%config InlineBackend.figure_formats = ['svg'] +``` + + + +# Hybrid Neutral Atom Routing and Mapping + +Neutral-atom (NA) processors combine long-range, native multi-qubit interactions with high-fidelity atom transport. +HyRoNA, the hybrid mapper in MQT QMAP, exploits both capabilities: it adaptively mixes gate-based routing (SWAP/BRIDGE) +with atom transport to minimize latency and error. It pairs interaction-aware initial placement with fast, +capability-specific cost models and an ASAP scheduler that respects hardware constraints, and it emits hardware-native +programs plus optional animation files for visualization. + +Below, we show how to use the Hybrid NA Mapper from Python on a small GHZ example and how to tune a few key parameters. + +## Example: GHZ state on a hybrid NA architecture + +In this example, we prepare an 8-qubit GHZ state (similar to the [zoned compiler](na_zoned_compiler.md)) and map it to a hybrid NA architecture. + +```{code-cell} ipython3 + +from qiskit import QuantumCircuit +# Build a compact GHZ(8) circuit (tree-like to keep depth small) + +qc = QuantumCircuit(8) +qc.h(0) +qc.cx(0, 4) +qc.cx(0, 2) +qc.cx(4, 6) +qc.cx(0, 1) +qc.cx(2, 3) +qc.cx(4, 5) +qc.cx(6, 7) + +qc.draw(output="mpl") + +``` + +### Load a hybrid NA architecture + +The hybrid mapper expects an architecture specification in JSON. +This repository ships several ready-to-use examples. + +```{code-cell} ipython3 + +from pathlib import Path +import tempfile +import os +from mqt.qmap.hybrid_mapper import NeutralAtomHybridArchitecture + +# Create a minimal architecture from an in-code JSON string and instantiate +# the NeutralAtomHybridArchitecture using a temporary file. The mapper's +# constructor expects a filename, so we write the JSON to a temp file first. +arch_json = '''{ + "name": "example arch", + "properties": { + "nRows": 5, + "nColumns": 5, + "nAods": 1, + "nAodCoordinates": 1, + "interQubitDistance": 10, + "minimalAodDistance": 0.1, + "interactionRadius": 1, + "blockingFactor": 1 + }, + "parameters": { + "nQubits": 10, + "gateTimes": { + "none": 0.5 + }, + "gateAverageFidelities": { + "none": 0.999 + }, + "decoherenceTimes": { + "t1": 100000000, + "t2": 1500000 + }, + "shuttlingTimes": { + "move": 0.55, + "aod_move": 0.55, + "aod_activate": 20, + "aod_deactivate": 20 + }, + "shuttlingAverageFidelities": { + "move": 1, + "aod_move": 1, + "aod_activate": 1, + "aod_deactivate": 1 + } + } +}''' + +with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as _tmp: + _tmp.write(arch_json) + tmp_name = _tmp.name + +arch = NeutralAtomHybridArchitecture(tmp_name) +# Clean up the temporary file +os.unlink(tmp_name) + +print(f"Loaded architecture: {arch.name} with {arch.num_qubits} qubits.") + +``` + +### Map with HyRoNA + +Mapping translates the algorithm to hardware-native operations using a combination of routing with SWAP/BRIDGE gates and +atom moves. What combination is used depends on the architecture capabilities and the mapper parameters. +Here, we show two examples: first, we only use gate-based routing, then we let the mapper decide freely. + +```{code-cell} ipython3 +from mqt.core import load +from mqt.qmap.hybrid_mapper import HybridNAMapper, MapperParameters + +# Optional: tweak parameters (defaults are sensible for most cases) +params_shuttling = MapperParameters() +params_shuttling.gate_weight = 1.0 +params_shuttling.shuttling_weight = 0.0 # disables atom moves + + +mapper = HybridNAMapper(arch, params=params_shuttling) + +# Convert the Qiskit circuit to an MQT QuantumComputation and map it +circ = load(qc) +mapper.map(circ) # optionally: mapper.map(circ, initial_mapping=InitialCircuitMapping.identity) + +# Retrieve mapping statistics +mapper.get_stats() +``` + +Note, how we set `shuttling_weight` zero to disallow atom moves and only use SWAP gates for routing. + +The idea of the hybrid mapper is to mix both capabilities or to automatically select the best one. +We now re-run the mapping with default parameters that allow both SWAPs and atom moves. + +```{code-cell} ipython3 +params_default = MapperParameters() + +mapper.set_parameters(params_default) + +mapper.map(circ) +mapper.get_stats() +``` + +Now the mapper uses atom moves only as they are the better option on this architecture where moves are unit fidelity and +gates are noisy. + +### Schedule the mapped circuit + +Scheduling orders the mapped operations as-soon-as-possible while respecting hardware constraints. + +```{code-cell} ipython3 +# Schedule; set create_animation_csv=True to generate visualization data +results = mapper.schedule(verbose=False, create_animation_csv=False) + +results +``` + +You can retrieve the mapped scheduled circuit (extended QASM2) and, if desired, the variant with explicit AOD movements equivalent to the operations done on the hardware. + +```{code-cell} ipython3 + +mapped_qasm = mapper.get_mapped_qc_qasm() +# Print a snippet of the mapped QASM +print("\n... Mapped QASM snippet ...\n" + + "\n".join(mapped_qasm.splitlines()[:30])) + +# AOD-annotated variant (hardware-native moves) +mapped_aod_qasm = mapper.get_mapped_qc_aod_qasm() +print("\n... AOD (converted) snippet ...\n" + "\n".join(mapped_aod_qasm.splitlines()[:30]) + "\n...") +``` + +Here, the `q` register corresponds to the 5x5=25 coordinates of the architecture and no longer to the circuit qubits. +The other registers are used for temporary storage and AOD control. + +The second variant shows explicit AOD movements that correspond to the atom moves done on hardware. +Here, the AODs can be activated, moved, and deactivated to shuttle atoms around. +The first entry corresponds to the x-direction and the second to the y-direction; in each pair, the two numbers denote start and end coordinates. + +### Export animation files (optional) + +HyRoNA can write animation output files that can be visualized with [MQT NAViz](https://github.com/munich-quantum-toolkit/naviz). +Typically one has to accelerate the shuttling speed for better visualization by setting `shuttling_speed_factor`. + +```{code-cell} ipython3 +# Re-run scheduling with animation output enabled +_ = mapper.schedule(verbose=False, create_animation_csv=True, shuttling_speed_factor=100) + +# Save the files; the method writes `.naviz` and `.namachine` files +# next to the given base name +mapper.save_animation_files("ghz8_hyrona_animation") +``` + +This creates `.namachine` and `.naviz` files which can then be imported into +MQT NAViz for visualization. + +![Animation](images/hybrid_shuttling.gif) + +## Tuning the mapper + +HyRoNA exposes a concise set of parameters via `MapperParameters`: + +- lookahead_depth: limited lookahead to peek at near-future layers +- lookahead_weight_swaps / lookahead_weight_moves: balance gate-routing vs. atom motion during lookahead +- decay: decreases the incentive for repeatedly blocking the same qubits +- gate_weight / shuttling_weight / shuttling_time_weight: cost-model weights for gates vs. transport +- dynamic_mapping_weight: bias for enabling dynamic re-mapping (SWAP/MOVE) when beneficial +- max_bridge_distance: limit for BRIDGE operations; use to avoid long-range chains +- initial_coord_mapping: strategy for hardware coordinate initialization + +For more details, please check the source code documentation of `MapperParameters`. + +## Tips + +- Initial mapping: you can explicitly select the initial circuit-to-hardware mapping for `map` or `map_qasm_file` using + `initial_mapping=InitialCircuitMapping.identity` or `InitialCircuitMapping.graph`. + +- QASM input: instead of building a circuit in Qiskit, you can call `mapper.map_qasm_file("path/to/circuit.qasm")` and + then `mapper.schedule(...)` as shown above. + +- Architectures: the examples `rubidium_hybrid.json`, `rubidium_gate.json`, and `rubidium_shuttling.json` in + `architectures/` cover different capability profiles and are a good starting point + +## Hybrid Synthesis Mapper + +The Hybrid Synthesis Mapper helps you compare and stitch together alternative circuit fragments while keeping track of the current mapping on the NA device. +Below is a compact example that mirrors the unit test flow and shows how to: + +- evaluate multiple candidate fragments and pick the best, +- append fragments, +- and retrieve mapped QASM as well as the AOD-annotated variant. + +First, we create the `HybridSynthesisMapper` and evaluate two candidate fragments (here, both are the same GHZ circuit as above for simplicity). + +```{code-cell} ipython3 +from mqt.qmap.hybrid_mapper import HybridSynthesisMapper + +# Reuse the architecture `arch` and GHZ circuit `qc` defined above. +synth = HybridSynthesisMapper(arch) +synth.init_mapping(qc.num_qubits) + +fidelities = synth.evaluate_synthesis_steps([circ, circ], also_map = False) +print("Fidelities of subcircuits:", fidelities ) +``` + +The fidelity return indicates how well the subcircuit can be appended to the circuit mapped to the hardware. + +You can then build up a larger program by appending fragments. + +```{code-cell} ipython3 +synth.append_with_mapping(circ) +``` + +This appends the circuit and maps it directly. This can be repeated for multiple fragments to always choose the best one. + +Similar to the normal Hybrid Mapper, you can retrieve the mapped circuit and the AOD-annotated variant: + +```{code-cell} ipython3 + +# Retrieve mapped circuit (extended QASM2) +qasm_mapped = synth.get_mapped_qc_qasm() +print("\n... Mapped QASM snippet ...\n" + + "\n".join(qasm_mapped.splitlines()[:30]) + + "\n...") +``` + +One can also access the circuits which is constructed step-by-step in a unmapped state (e.g. to use a different mapper). + +```{code-cell} ipython3 +qasm_synth = synth.get_synthesized_qc_qasm() +print("\n... Synthesized QASM snippet ...\n" + + "\n".join(qasm_synth.splitlines()[:30]) + + "\n...") +``` + +Here this corresponds simply to the input GHZ circuit. + +This workflow lets you mix candidate-generation (synthesis) with hardware-aware mapping and scheduling, while keeping a single source of truth for mapping state across fragments. diff --git a/include/hybridmap/HardwareQubits.hpp b/include/hybridmap/HardwareQubits.hpp index 660fb854a..6b88fa914 100644 --- a/include/hybridmap/HardwareQubits.hpp +++ b/include/hybridmap/HardwareQubits.hpp @@ -19,10 +19,12 @@ #include "ir/operations/Operation.hpp" #include +#include #include #include #include #include +#include #include #include #include @@ -31,69 +33,85 @@ namespace na { /** - * @brief Class that represents the hardware qubits of a neutral atom quantum - * @details Class that represents the hardware qubits of a neutral atom quantum - * computer. It stores the mapping from the circuit qubits to the hardware - * qubits and the mapping from the hardware qubits to the coordinates of the - * neutral atoms. It also stores the swap distances between the hardware - * qubits. + * @brief Represents hardware qubits and their placement on a neutral atom + * device. + * @details Manages mapping (hardware qubit -> coordinate), maintains cached + * swap distances and nearby-qubit sets derived from the architecture + * interaction radius, and provides utilities for movement, neighborhood + * queries, blocking analysis, and coordinate-based path heuristics. */ class HardwareQubits { protected: - const NeutralAtomArchitecture* arch; + const NeutralAtomArchitecture* arch = nullptr; + CoordIndex nQubits = 0; qc::Permutation hwToCoordIdx; qc::SymmetricMatrix swapDistances; std::map nearbyQubits; + std::vector freeCoordinates; + std::vector occupiedCoordinates; qc::Permutation initialHwPos; /** - * @brief Initializes the swap distances between the hardware qubits for the - * trivial initial layout. - * @details Initializes the swap distances between the hardware qubits. This - * is only valid for the trivial initial layout. + * @brief Precompute swap distances for trivial initial layout. + * @details Fills symmetric matrix with shortest-path distances (in edges) + * when coordinates align with hardware indices. */ void initTrivialSwapDistances(); /** - * @brief Initializes the nearby qubits for each hardware qubit. - * @details Nearby qubits are the qubits that are closer than the interaction - * radius. Therefore they can be swapped with a single swap operation. + * @brief Initialize per-qubit nearby sets (within interaction radius). + * @details Nearby qubits are those reachable by a single + * entangling/interaction edge and hence by one swap. */ void initNearbyQubits(); /** - * @brief Computes the nearby qubits for a single hardware qubit. - * @details Computes the nearby qubits for a single hardware qubit. This - * function is called by initNearbyQubits(). It uses the nearby coordinates - * of the neutral atom architecture to compute the nearby qubits. - * @param qubit The hardware qubit for which the nearby qubits are computed. + * @brief Populate nearby set for one qubit. + * @param qubit Hardware qubit whose neighbors are computed. */ void computeNearbyQubits(HwQubit qubit); /** - * @brief Computes the swap distance between two hardware qubits. - * @details Computes the swap distance between two hardware qubits. This - * function is called by getSwapDistance(). It uses a breadth-first search - * to find the shortest path between the two qubits. - * @param q1 The first hardware qubit. - * @param q2 The second hardware qubit. + * @brief Compute swap distance (BFS) between two hardware qubits and cache + * it. + * @param q1 First hardware qubit. + * @param q2 Second hardware qubit. */ void computeSwapDistance(HwQubit q1, HwQubit q2); /** - * @brief Resets the swap distances between the hardware qubits. - * @details Used after each shuttling operation to reset the swap distances. + * @brief Invalidate all cached swap distances (set entries to -1). + * @note Call after shuttling alters physical adjacency relevance. */ void resetSwapDistances(); public: // Constructors - HardwareQubits(const NeutralAtomArchitecture& architecture, - InitialCoordinateMapping initialCoordinateMapping, - uint32_t seed) - : arch(&architecture), swapDistances(architecture.getNqubits()) { + HardwareQubits() = default; + /** + * @brief Construct hardware qubit layout and caches. + * @param architecture Device architecture reference. + * @param nQubits Number of hardware qubits to track. + * @param initialCoordinateMapping Initial placement strategy + * (Trivial/Random). + * @param seed RNG seed for Random; if 0 uses std::random_device(). + * @details Populates hw->coord mapping, nearby sets, occupied/free coordinate + * lists; precomputes swap distances for trivial mapping, leaves them lazy + * (-1) for random mapping. + */ + explicit HardwareQubits( + const NeutralAtomArchitecture& architecture, const CoordIndex nQubits = 0, + const InitialCoordinateMapping initialCoordinateMapping = Trivial, + uint32_t seed = 0) + : arch(&architecture), nQubits(nQubits) { + + assert(nQubits <= architecture.getNpositions() && + "Number of hardware qubits exceeds available positions."); + swapDistances = qc::SymmetricMatrix(nQubits); + switch (initialCoordinateMapping) { case Trivial: - for (uint32_t i = 0; i < architecture.getNqubits(); ++i) { + for (uint32_t i = 0; i < nQubits; ++i) { hwToCoordIdx.emplace(i, i); + occupiedCoordinates.emplace_back(i); } initTrivialSwapDistances(); break; @@ -104,41 +122,95 @@ class HardwareQubits { seed = std::random_device()(); } std::mt19937 g(seed); - std::shuffle(indices.begin(), indices.end(), g); - for (uint32_t i = 0; i < architecture.getNqubits(); ++i) { + std::ranges::shuffle(indices, g); + for (uint32_t i = 0; i < nQubits; ++i) { hwToCoordIdx.emplace(i, indices[i]); + occupiedCoordinates.emplace_back(indices[i]); } - swapDistances = qc::SymmetricMatrix(architecture.getNqubits(), -1); + swapDistances = qc::SymmetricMatrix(nQubits, -1); } initNearbyQubits(); + + for (uint32_t i = 0; i < architecture.getNpositions(); ++i) { + if (std::ranges::find(occupiedCoordinates, i) == + occupiedCoordinates.end()) { + freeCoordinates.emplace_back(i); + } + } + initialHwPos = hwToCoordIdx; } - // Mapping + /** + * @brief Enumerate all minimal-length paths between two qubits. + * @details BFS builds predecessor layers; all shortest sequences q1->...->q2 + * are returned. + * @param q1 Source hardware qubit. + * @param q2 Target hardware qubit. + * @return Vector of shortest paths, each path a vector of hardware qubits. + */ + [[nodiscard]] std::vector + computeAllShortestPaths(HwQubit q1, HwQubit q2) const; + + /** + * @brief Number of hardware qubits tracked. + * @return Hardware qubit count. + */ + [[nodiscard]] CoordIndex getNumQubits() const { return nQubits; } /** - * @brief Checks if a hardware qubit is mapped to a coordinate. - * @param idx The coordinate index. - * @return Boolean indicating if the hardware qubit is mapped to a coordinate. + * @brief Check whether a coordinate is currently occupied. + * @param idx Coordinate index. + * @return True if occupied by some hardware qubit; false otherwise. */ - [[nodiscard]] bool isMapped(CoordIndex idx) const { - return !std::none_of( - hwToCoordIdx.begin(), hwToCoordIdx.end(), - [idx](const auto& pair) { return pair.second == idx; }); + [[nodiscard]] bool isMapped(const CoordIndex idx) const { + return std::ranges::find(occupiedCoordinates, idx) != + occupiedCoordinates.end(); } /** - * @brief Updates mapping after moving a hardware qubit to a coordinate. - * @details Checks if the coordinate is valid and free. If yes, the mapping is - * updated. - * @param hwQubit The hardware qubit to be moved. - * @param newCoord The new coordinate of the hardware qubit. + * @brief Move a hardware qubit to a new coordinate. + * @details Validates destination free, updates mapping and occupancy sets, + * recalculates affected nearby sets, invalidates swap distance cache. + * @param hwQubit Hardware qubit identifier. + * @param newCoord Destination coordinate index. + * @throw std::runtime_error If newCoord invalid or already occupied. */ void move(HwQubit hwQubit, CoordIndex newCoord); /** - * @brief Converts gate qubits from hardware qubits to coordinate indices. - * @param op The operation. + * @brief Remove a hardware qubit from mapping, occupancy and nearby caches. + * @details Invalidates all swap distances involving the qubit and erases it + * from neighbor sets. + * @param hwQubit Hardware qubit to remove. + */ + void removeHwQubit(const HwQubit hwQubit) { + const auto currentCoord = hwToCoordIdx.at(hwQubit); + hwToCoordIdx.erase(hwQubit); + initialHwPos.erase(hwQubit); + + if (auto it = std::ranges::find(occupiedCoordinates, currentCoord); + it != occupiedCoordinates.end()) { + occupiedCoordinates.erase(it); + } + if (std::ranges::find(freeCoordinates, currentCoord) == + freeCoordinates.end()) { + freeCoordinates.emplace_back(currentCoord); + } + // set swap distances to -1 + for (uint32_t i = 0; i < swapDistances.size(); ++i) { + swapDistances(hwQubit, i) = -1; + swapDistances(i, hwQubit) = -1; + } + nearbyQubits.erase(hwQubit); + for (auto& nearby : nearbyQubits | std::views::values) { + nearby.erase(hwQubit); + } + } + + /** + * @brief Rewrite operation qubit indices from hardware IDs to coordinates. + * @param op Operation pointer (modified in place). */ void mapToCoordIdx(qc::Operation* op) const { op->setTargets(hwToCoordIdx.apply(op->getTargets())); @@ -148,34 +220,45 @@ class HardwareQubits { } /** - * @brief Returns the coordinate index of a hardware qubit. - * @param qubit The hardware qubit. - * @return The coordinate index of the hardware qubit. + * @brief Coordinate index mapped to a hardware qubit. + * @param qubit Hardware qubit. + * @return Coordinate index. + * @throw std::out_of_range If qubit not in mapping. */ - [[nodiscard]] CoordIndex getCoordIndex(HwQubit qubit) const { + [[nodiscard]] CoordIndex getCoordIndex(const HwQubit qubit) const { return hwToCoordIdx.at(qubit); } /** - * @brief Returns the coordinate indices of a set of hardware qubits. - * @param hwQubits The set of hardware qubits. - * @return The coordinate indices of the hardware qubits. + * @brief Coordinate indices for a set of hardware qubits. + * @param hwQubits Set of hardware qubits. + * @return Set of coordinate indices. */ [[nodiscard]] std::set - getCoordIndices(std::set& hwQubits) const { + getCoordIndices(const std::set& hwQubits) const { std::set coordIndices; for (auto const& hwQubit : hwQubits) { - coordIndices.emplace(this->getCoordIndex(hwQubit)); + coordIndices.emplace(getCoordIndex(hwQubit)); + } + return coordIndices; + } + + [[nodiscard]] std::vector + getCoordIndices(const std::vector& hwQubits) const { + std::vector coordIndices; + coordIndices.reserve(hwQubits.size()); + for (auto const& hwQubit : hwQubits) { + coordIndices.emplace_back(getCoordIndex(hwQubit)); } return coordIndices; } + /** - * @brief Returns the hardware qubit at a coordinate. - * @details Returns the hardware qubit at a coordinate. Throws an exception if - * there is no hardware qubit at the coordinate. - * @param coordIndex The coordinate index. - * @return The hardware qubit at the coordinate. + * @brief Hardware qubit occupying a coordinate. + * @param coordIndex Coordinate index. + * @return Hardware qubit ID. + * @throw std::runtime_error If no qubit occupies the coordinate. */ - [[nodiscard]] HwQubit getHwQubit(CoordIndex coordIndex) const { + [[nodiscard]] HwQubit getHwQubit(const CoordIndex coordIndex) const { for (auto const& [hwQubit, index] : hwToCoordIdx) { if (index == coordIndex) { return hwQubit; @@ -185,33 +268,18 @@ class HardwareQubits { std::to_string(coordIndex)); } - // Forwards from architecture class - - /** - * @brief Returns the nearby coordinates of a hardware qubit. - * @param q The hardware qubit. - * @return The nearby coordinates of the hardware qubit. - */ - [[nodiscard]] [[maybe_unused]] std::set - getNearbyCoordinates(HwQubit q) const { - return this->arch->getNearbyCoordinates(this->getCoordIndex(q)); - } - // Swap Distances and Nearby qc::Qubits /** - * @brief Returns the swap distance between two hardware qubits. - * @details Returns the swap distance between two hardware qubits. If the - * swap distance is not yet computed, it is computed using a breadth-first - * search. - * @param q1 The first hardware qubit. - * @param q2 The second hardware qubit. - * @param closeBy If the swap should be performed to the exact position of q2 - * or just to its vicinity. - * @return The swap distance between the two hardware qubits. + * @brief Swap distance between two hardware qubits (lazy cached). + * @param q1 First qubit. + * @param q2 Second qubit. + * @param closeBy If false, allow stopping adjacent to q2 (adds 1 to + * distance). + * @return Distance in number of swaps (0 if identical). */ - [[nodiscard]] SwapDistance getSwapDistance(HwQubit q1, HwQubit q2, - bool closeBy = true) { + [[nodiscard]] SwapDistance getSwapDistance(const HwQubit q1, const HwQubit q2, + const bool closeBy = true) { if (q1 == q2) { return 0; } @@ -225,69 +293,81 @@ class HardwareQubits { } /** - * @brief Returns the nearby hardware qubits of a hardware qubit. - * @param q The hardware qubit. - * @return The nearby hardware qubits of the hardware qubit. + * @brief Nearby hardware qubits (within interaction radius). + * @param q Hardware qubit. + * @return Set of nearby qubits. */ - [[nodiscard]] HwQubits getNearbyQubits(HwQubit q) const { + [[nodiscard]] HwQubits getNearbyQubits(const HwQubit q) const { return nearbyQubits.at(q); } /** - * @brief Returns vector of all possible swaps for a hardware qubit. - * @param q The hardware qubit. - * @return The vector of all possible swaps for the hardware qubit. + * @brief All possible immediate swaps (pairs with each nearby qubit). + * @param q Hardware qubit. + * @return Vector of swap pairs. */ [[nodiscard]] std::vector getNearbySwaps(HwQubit q) const; /** - * @brief Returns the unoccupied coordinates in the vicinity of a coordinate. - * @param idx The coordinate index. - * @return The unoccupied coordinates in the vicinity of the coordinate. + * @brief Unoccupied coordinates within interaction radius of a coordinate. + * @param idx Coordinate index. + * @return Set of free coordinate indices nearby. */ - std::set getNearbyFreeCoordinatesByCoord(CoordIndex idx); + [[nodiscard]] std::set + getNearbyFreeCoordinatesByCoord(CoordIndex idx) const; /** - * @brief Returns the occupied coordinates in the vicinity of a coordinate. - * @param idx The coordinate index. - * @return The occupied coordinates in the vicinity of the coordinate. + * @brief Occupied coordinates within interaction radius of a coordinate. + * @param idx Coordinate index. + * @return Set of occupied coordinate indices nearby. */ [[nodiscard]] std::set getNearbyOccupiedCoordinatesByCoord(CoordIndex idx) const; /** - * @brief Computes the summed swap distance between all hardware qubits in a - * set. - * @param qubits The set of hardware qubits. - * @return The summed swap distance between all hardware qubits in the set. + * @brief Sum of pairwise swap distances among a set of qubits. + * @param qubits Set of hardware qubits (modified if needed for caching). + * @return Summed distances; for two qubits equals their distance. */ qc::fp getAllToAllSwapDistance(std::set& qubits); /** - * @brief Computes the closest free coordinate in a given direction. - * @details Uses a breadth-first search to find the closest free coordinate in - * a given direction. - * @param qubit The hardware qubit to start the search from. - * @param direction The direction in which the search is performed - * (Left/Right, Down/Up) - * @param excludedCoords Coordinates to be ignored in the search. - * @return The closest free coordinate in the given direction. + * @brief Find closest free coordinate in a direction, else return all free. + * @param coord Starting coordinate. + * @param direction Direction sign (per dimension) encapsulated in Direction. + * @param excludedCoords Coordinates to ignore. + * @return Singleton vector with nearest directional free coordinate or all + * free coordinates if none in direction. */ - std::vector - findClosestFreeCoord(HwQubit qubit, Direction direction, - const CoordIndices& excludedCoords = {}); + [[nodiscard]] std::vector + findClosestFreeCoord(CoordIndex coord, Direction direction, + const CoordIndices& excludedCoords = {}) const; + + /** + * @brief Hardware qubit closest by Euclidean distance to a coordinate, + * excluding some. + * @param coord Target coordinate index. + * @param ignored Set of qubits to ignore. + * @return Closest hardware qubit ID. + */ + [[nodiscard]] HwQubit getClosestQubit(CoordIndex coord, + const HwQubits& ignored) const; // Blocking /** - * @brief Computes all hardware qubits that are blocked by a set of hardware - * qubits. - * @param qubits The input hardware qubits. - * @return The blocked hardware qubits. + * @brief Hardware qubits blocked by interactions of given qubits. + * @param qubits Active hardware qubits. + * @return Set of hardware qubits considered blocked. */ - std::set getBlockedQubits(const std::set& qubits); + [[nodiscard]] std::set + getBlockedQubits(const std::set& qubits) const; - [[nodiscard]] std::map getInitHwPos() const { - std::map initialHwPosMap; + /** + * @brief Initial hardware->coordinate mapping from construction time. + * @return Map of hardware qubit to initial coordinate index. + */ + [[nodiscard]] std::map getInitHwPos() const { + std::map initialHwPosMap; for (auto const& pair : initialHwPos) { initialHwPosMap[pair.first] = pair.second; } diff --git a/include/hybridmap/HybridAnimation.hpp b/include/hybridmap/HybridAnimation.hpp index 87bb0c822..0a8f2c930 100644 --- a/include/hybridmap/HybridAnimation.hpp +++ b/include/hybridmap/HybridAnimation.hpp @@ -15,51 +15,84 @@ #include "ir/Definitions.hpp" #include "ir/operations/Operation.hpp" -#include #include #include #include #include namespace na { +/** + * @brief Helper to generate NaViz-style animation strings for neutral-atom + * layouts. + * @details Maintains bidirectional mappings between coordinate indices and + * atom identifiers, as well as continuous coordinates derived from the + * architecture's grid geometry. Provides utilities to emit initial placement + * lines and per-operation animation snippets. + * @note The architecture reference passed to the constructor must remain valid + * for the lifetime of this object. + */ class AnimationAtoms { - using axesId = std::uint32_t; - using marginId = std::uint32_t; - protected: - uint32_t colorSlm = 0; - uint32_t colorAod = 1; - uint32_t colorLocal = 2; - [[maybe_unused]] uint32_t colorGlobal = 3; - uint32_t colorCz = 4; - + /** Map from discrete coordinate index to atom id. */ std::map coordIdxToId; + /** Map from atom id to continuous (x,y) coordinates. */ std::map> idToCoord; - std::map axesIds; - std::map marginIds; - uint32_t axesIdCounter = 0; - uint32_t marginIdCounter = 0; + const NeutralAtomArchitecture* arch; - axesId addAxis(HwQubit id); - void removeAxis(HwQubit id) { axesIds.erase(id); } - marginId addMargin(HwQubit id); - void removeMargin(HwQubit id) { marginIds.erase(id); } + /** + * @brief Initialize coordinate/id maps from initial hardware and ancilla + * positions. + * @details For hardware qubits, places atoms at grid points based on + * architecture columns and inter-qubit distance. Flying ancilla qubits are + * offset from the grid using a small per-index displacement that depends on + * the architecture's AOD intermediate levels, and their id space follows the + * hardware ids. + * @param initHwPos Initial mapping of hardware qubit id -> coordinate index. + * @param initFaPos Initial mapping of flying-ancilla id -> coordinate index. + */ + void initPositions(const std::map& initHwPos, + const std::map& initFaPos); public: - AnimationAtoms(const std::map& initHwPos, - const NeutralAtomArchitecture& arch); + /** + * @brief Construct animation helper with initial positions. + * @param initHwPos Initial hardware id -> coordinate index mapping. + * @param initFaPos Initial flying-ancilla id -> coordinate index mapping. + * @param architecture Reference to the neutral atom architecture providing + * grid geometry and distances. + */ + AnimationAtoms(const std::map& initHwPos, + const std::map& initFaPos, + const NeutralAtomArchitecture& architecture) + : arch(&architecture) { + initPositions(initHwPos, initFaPos); + } - std::string getInitString(); - std::string getEndString(qc::fp endTime); - static std::string createCsvLine(qc::fp startTime, HwQubit id, qc::fp x, - qc::fp y, uint32_t size = 1, - uint32_t color = 0, bool axes = false, - axesId axId = 0, bool margin = false, - marginId marginId = 0, - qc::fp marginSize = 0); - std::string createCsvOp(const std::unique_ptr& op, - qc::fp startTime, qc::fp endTime, - const NeutralAtomArchitecture& arch); + /** + * @brief Emit NaViz lines that place all initial atoms. + * @details One line per atom with absolute coordinates and an atom label + * (e.g., "atom (x, y) atom"). + * @return Concatenated lines suitable for a NaViz animation file. + */ + [[nodiscard]] std::string placeInitAtoms() const; + /** + * @brief Convert a quantum operation into NaViz animation commands. + * @details Supports AOD load/store/move operations and standard gates. + * - AodActivate: emits a load block with listed atoms. + * - AodDeactivate: emits a store block with listed atoms. + * - AodMove: updates internal coordinates by matching AOD start/end lists and + * emits move commands for affected atoms; also updates coordIdxToId for + * origin/target coordinate-index pairs. + * - Multi-qubit gates: emits a grouped cz with all involved atoms. + * - Single-qubit gates: emits a simple rz 1 for the target atom (independent + * of the exact gate -> does not matter for visualization). + * - Other operations are currently not supported for animation output. + * @param op The operation to translate (uses coordinate indices as qubits). + * @param startTime The animation timestamp to annotate the command with. + * @return NaViz command string for the operation at the given time. + */ + std::string opToNaViz(const std::unique_ptr& op, + qc::fp startTime); }; } // namespace na diff --git a/include/hybridmap/HybridNeutralAtomMapper.hpp b/include/hybridmap/HybridNeutralAtomMapper.hpp index 5f7881a0c..bf369ee7f 100644 --- a/include/hybridmap/HybridNeutralAtomMapper.hpp +++ b/include/hybridmap/HybridNeutralAtomMapper.hpp @@ -10,17 +10,18 @@ #pragma once -#include "NeutralAtomLayer.hpp" +#include "circuit_optimizer/CircuitOptimizer.hpp" #include "hybridmap/HardwareQubits.hpp" #include "hybridmap/Mapping.hpp" #include "hybridmap/NeutralAtomArchitecture.hpp" #include "hybridmap/NeutralAtomDefinitions.hpp" +#include "hybridmap/NeutralAtomLayer.hpp" #include "hybridmap/NeutralAtomScheduler.hpp" #include "hybridmap/NeutralAtomUtils.hpp" #include "ir/Definitions.hpp" #include "ir/QuantumComputation.hpp" -#include "ir/operations/Operation.hpp" +#include #include #include #include @@ -28,57 +29,72 @@ #include #include #include +#include #include -#include +#include #include namespace na { /** - * @brief Struct to store the runtime parameters of the mapper. + * @brief Runtime configuration parameters for the neutral atom mapper. + * @details Tunable weights and limits guiding swap vs. shuttling vs. ancilla + * decisions, lookahead, and stochastic initialization. */ struct MapperParameters { + uint32_t lookaheadDepth = 1; qc::fp lookaheadWeightSwaps = 0.1; qc::fp lookaheadWeightMoves = 0.1; qc::fp decay = 0.1; - qc::fp shuttlingTimeWeight = 1; + qc::fp shuttlingTimeWeight = 0.1; + qc::fp dynamicMappingWeight = 1; qc::fp gateWeight = 1; qc::fp shuttlingWeight = 1; - uint32_t seed = 0; + uint32_t seed = 42; + uint32_t numFlyingAncillas = 0; + uint32_t limitShuttlingLayer = 10; + uint32_t maxBridgeDistance = 1; + bool usePassBy = true; bool verbose = false; - InitialCoordinateMapping initialMapping = InitialCoordinateMapping::Trivial; + InitialCoordinateMapping initialCoordMapping = Trivial; }; /** - * @brief Class to map a quantum circuit to a neutral atom architecture. - * @details The mapping has following important parts: - * - initial mapping: The initial mapping of the circuit qubits to the hardware - * qubits. - * - layer creation: The creation of the front and lookahead layers, done one - * the fly and taking into account basic commutation rules. - * - estimation: The estimation of the number of swap gates and moves needed to - * execute a given gate and the decision which technique is better. - * - gate based mapping: SABRE based algorithm to choose the bast swap for the - * given layers. - * - shuttling based mapping: Computing and evaluation of possible moves and - * choosing best. - * - multi-qubit-gates: Additional steps and checks to bring multiple qubits - * together. - * -> Final circuit contains abstract SWAP gates and MOVE operations, which need - * to be decomposed using AODScheduler. + * @brief Aggregated counters collected during mapping. + */ +struct MapperStats { + uint32_t nSwaps = 0; ///< Number of executed SWAP gates. + uint32_t nBridges = 0; ///< Number of bridge operations. + uint32_t nFAncillas = 0; ///< Number of flying ancilla usages. + uint32_t nMoves = 0; ///< Number of MOVE operations. + uint32_t nPassBy = 0; ///< Number of pass-by combinations. +}; + +/** + * @brief Maps a quantum circuit onto a neutral atom architecture using hybrid + * routing. + * @details Combines gate-based (swap/bridge) and shuttling-based + * (move/pass-by/flying ancilla) strategies. Workflow: + * 1. Initialize mapping and hardware placement. + * 2. Build front/lookahead layers using commutation rules. + * 3. Estimate routing costs (swap vs. move vs. bridge/flying ancilla). + * 4. Select best primitive via weighted cost functions (SABRE-inspired + * heuristic). + * 5. Handle multi-qubit gate positioning. + * 6. Produce circuit with abstract SWAP and MOVE operations (later decomposed + * to AOD level). */ class NeutralAtomMapper { protected: + MapperStats stats; // The considered architecture - const NeutralAtomArchitecture& arch; + const NeutralAtomArchitecture* arch; // The mapped quantum circuit qc::QuantumComputation mappedQc; // The mapped quantum circuit converted to AOD movements qc::QuantumComputation mappedQcAOD; // The scheduler to schedule the mapped quantum circuit NeutralAtomScheduler scheduler; - // The gates that have been executed - std::vector executedCommutingGates; // Gates in the front layer to be executed with swap gates GateList frontLayerGate; // Gates in the front layer to be executed with move operations @@ -93,309 +109,416 @@ class NeutralAtomMapper { MapperParameters parameters; // The qubits that are blocked by the last swap std::deque> lastBlockedQubits; + // The last swap that has been executed + Swap lastSwap = {0, 0}; // The last moves that have been executed std::deque lastMoves; // Precomputed decay weights std::vector decayWeights; - // Counter variables - uint32_t nSwaps = 0; - uint32_t nMoves = 0; + // indicates if multi-qubit gates are in the circuit + bool multiQubitGates = false; // The current placement of the hardware qubits onto the coordinates HardwareQubits hardwareQubits; + HardwareQubits flyingAncillas; // The current mapping between circuit qubits and hardware qubits Mapping mapping; // Methods for mapping + /** - * @brief Maps the gate to the mapped quantum circuit. - * @param op The gate to map + * @brief Append a single operation to the mapped circuit applying current + * qubit mapping. + * @param op Operation (from original circuit) to map. */ void mapGate(const qc::Operation* op); /** - * @brief Maps all currently possible gates and updated until no more gates - * can be mapped. - * @param layer The layer to map all possible gates for + * @brief Iteratively map all executable gates until front layer stalls. + * @param frontLayer Current front layer container. + * @param lookaheadLayer Lookahead layer container. */ - void mapAllPossibleGates(NeutralAtomLayer& layer); + void mapAllPossibleGates(NeutralAtomLayer& frontLayer, + NeutralAtomLayer& lookaheadLayer); /** - * @brief Returns all gates that can be executed now - * @param gates The gates to be checked - * @return All gates that can be executed now + * @brief Filter a list to gates executable under current mapping. + * @param gates Candidate gates. + * @return Subset of executable gates. */ GateList getExecutableGates(const GateList& gates); /** - * @brief Checks if the given gate can be executed for the given mapping and - * hardware arrangement. - * @param opPointer The gate to check - * @return True if the gate can be executed, false otherwise + * @brief Check gate executability given current mapping and placement. + * @param opPointer Gate to test. + * @return True if gate can be applied now; false otherwise. */ bool isExecutable(const qc::Operation* opPointer); + void updateBlockedQubits(const Swap& swap) { + const HwQubits qubits = {swap.first, swap.second}; + updateBlockedQubits(qubits); + } + void updateBlockedQubits(const HwQubits& qubits); + /** - * @brief Update the mapping for the given swap gate. - * @param swap The swap gate to update the mapping for + * @brief Apply a SWAP to update logical↔hardware mapping state. + * @param swap Hardware qubit pair. */ - void updateMappingSwap(Swap swap); + void applySwap(const Swap& swap); /** - * @brief Update the mapping for the given move operation. - * @param move The move operation to update the mapping for + * @brief Apply a MOVE (shuttling) operation updating placement & mapping. + * @param move Move descriptor. */ - void updateMappingMove(AtomMove move); + void applyMove(AtomMove move); + + void applyBridge(NeutralAtomLayer& frontLayer, const Bridge& bridge); + void applyFlyingAncilla(NeutralAtomLayer& frontLayer, + const FlyingAncillaComb& faComb); + void applyPassBy(NeutralAtomLayer& frontLayer, const PassByComb& pbComb); // Methods for gate vs. shuttling /** - * @brief Assigns the given gates to the gate or shuttling layers. - * @param frontGates The gates to be assigned to the front layers - * @param lookaheadGates The gates to be assigned to the lookahead layers + * @brief Partition gates into gate-routing vs. shuttling lists for each + * layer. + * @param frontGates Gates in current front layer. + * @param lookaheadGates Gates in lookahead layer. */ void reassignGatesToLayers(const GateList& frontGates, const GateList& lookaheadGates); + /** - * @brief Estimates the minimal number of swap gates and time needed to - * execute the given gate. - * @param opPointer The gate to estimate the number of swap gates and time for - * @return The minimal number of swap gates and time needed to execute the - * given gate + * @brief Advance one step using gate-based routing (swaps/bridges). + * @param frontLayer Front layer container. + * @param lookaheadLayer Lookahead layer container. + * @param i Index of the considered operation in the front layer. + * @return Number of mapped operations performed. + */ + size_t gateBasedMapping(NeutralAtomLayer& frontLayer, + NeutralAtomLayer& lookaheadLayer, size_t i); + /** + * @brief Advance one step using shuttling primitives (moves/pass-by/flying + * ancilla). + * @param frontLayer Front layer container. + * @param lookaheadLayer Lookahead layer container. + * @param i Index of the considered operation in the front layer. + * @return Number of mapped operations performed. + */ + size_t shuttlingBasedMapping(NeutralAtomLayer& frontLayer, + NeutralAtomLayer& lookaheadLayer, size_t i); + + /** + * @brief Estimate swaps and time required to execute a gate via swapping. + * @param opPointer Gate under consideration. + * @return Pair (#swaps, estimated time cost). */ std::pair estimateNumSwapGates(const qc::Operation* opPointer); /** - * @brief Estimates the minimal number of move operations and time needed to - * execute the given gate. - * @param opPointer The gate to estimate the number of move operations and - * time for - * @return The minimal number of move operations and time needed to execute - * the given gate + * @brief Estimate MOVE count and time for executing a gate via shuttling. + * @param opPointer Gate under consideration. + * @return Pair (#moves, estimated time cost). */ - std::pair estimateNumMove(const qc::Operation* opPointer); + std::pair + estimateNumMove(const qc::Operation* opPointer) const; /** - * @brief Uses estimateNumSwapGates and estimateNumMove to decide if a swap - * gate or move operation is better. - * @param opPointer The gate to estimate the number of swap gates and move - * operations for - * @return True if a swap gate is better, false if a move operation is better + * @brief Compare swap vs. move estimates to choose routing primitive. + * @param opPointer Target gate. + * @return True if swap-based routing chosen; false for move-based. */ bool swapGateBetter(const qc::Operation* opPointer); // Methods for swap gates mapping /** - * @brief Finds the best swap gate for the front layer. - * @details The best swap gate is the one that minimizes the cost function. - * This takes into account close by swaps from two-qubit gates and exact moves - * from multi-qubit gates. - * @return The best swap gate for the front layer + * @brief Select best swap minimizing composite cost (distance + decay). + * @param lastSwapUsed Previously applied swap (for decay context). + * @return Best swap candidate. */ - Swap findBestSwap(const Swap& lastSwap); + Swap findBestSwap(const Swap& lastSwapUsed); /** - * @brief Returns all possible swap gates for the front layer. - * @details The possible swap gates are all swaps starting from qubits in the - * front layer. - * @return All possible swap gates for the front layer + * @brief Enumerate candidate swaps derived from front layer proximity. + * @param swapsFront Pair of (close-by swaps, weighted exact swaps). + * @return Set of swap candidates. */ [[nodiscard]] std::set getAllPossibleSwaps(const std::pair& swapsFront) const; + // Methods for bridge operations mapping + /** - * @brief Returns the next best shuttling move operation for the front layer. - * @return The next best shuttling move operation for the front layer + * @brief Choose best bridge operation given current best swap. + * @param bestSwap Candidate swap used for context. + * @return Selected bridge descriptor. */ + [[nodiscard]] Bridge findBestBridge(const Swap& bestSwap); + /** + * @brief Compute shortest bridge circuits compatible with a swap candidate. + * @param bestSwap Swap context. + * @return List of shortest bridges. + */ + [[nodiscard]] Bridges getShortestBridges(const Swap& bestSwap); + + /** + * @brief Current coordinate usage (occupied indices) snapshot. + * @return Set of occupied coordinate indices. + */ + [[nodiscard]] CoordIndices computeCurrentCoordUsages() const; + + /** + * @brief Convert a MOVE combination to a flying ancilla combination if + * suitable. + * @param moveComb MOVE combination candidate. + * @return Equivalent flying-ancilla combination. + */ + [[nodiscard]] FlyingAncillaComb + convertMoveCombToFlyingAncillaComb(const MoveComb& moveComb) const; + /** + * @brief Convert a MOVE combination to a pass-by combination if suitable. + * @param moveComb MOVE combination candidate. + * @return Equivalent pass-by combination. + */ + [[nodiscard]] PassByComb + convertMoveCombToPassByComb(const MoveComb& moveComb) const; // Methods for shuttling operations mapping /** - * @brief Finds the current best move operation based on the cost function. - * @details Uses getAllMoveCombinations to find all possible move combinations - * (direct move, move away, multi-qubit moves) and then chooses the best one - * based on the cost function. - * @return The current best move operation + * @brief Select best MOVE combination using cost (distance reduction + + * parallelization). + * @return Best move combination descriptor. */ - AtomMove findBestAtomMove(); + MoveComb findBestAtomMove(); + // std::pair findBestAtomMoveWithOp(); /** - * @brief Returns all possible move combinations for the front layer. - * @details This includes direct moves, move away and multi-qubit moves. - * Only move combinations with minimal number of moves are kept. - * @return Vector of possible move combinations for the front layer + * @brief Generate all minimal MOVE combinations (direct/away/multi-qubit). + * @return List of viable combinations. */ MoveCombs getAllMoveCombinations(); /** - * @brief Returns all possible move away combinations for a move from start to - * target. - * @details The possible move away combinations are all combinations of move - * operations that move qubits away and then the performs the actual move - * operation. The move away is chosen such that it is in the same direction as - * the second move operation. - * @param start The start position of the actual move operation - * @param target The target position of the actual move operation - * @param excludedCoords Coordinates the qubits should not be moved to - * @return All possible move away combinations for a move from start to target + * @brief Enumerate staged move-away then move-to-target combinations. + * @param startCoord Origin coordinate of final move. + * @param targetCoord Destination coordinate. + * @param excludedCoords Coordinates disallowed for interim moves. + * @return Candidate move-away sequences. + */ + [[nodiscard]] MoveCombs + getMoveAwayCombinations(CoordIndex startCoord, CoordIndex targetCoord, + const CoordIndices& excludedCoords) const; + + // Methods for flying ancilla operations mapping + /** + * @brief Compare swap vs. bridge and select routing method. + * @param bestSwap Swap candidate. + * @param bestBridge Bridge candidate. + * @return Chosen mapping method. */ - MoveCombs getMoveAwayCombinations(CoordIndex start, CoordIndex target, - const CoordIndices& excludedCoords); + [[nodiscard]] MappingMethod compareSwapAndBridge(const Swap& bestSwap, + const Bridge& bestBridge); + /** + * @brief Compare shuttling vs. flying ancilla vs. pass-by for a move + * candidate. + * @param bestMoveComb Best move combination. + * @param bestFaComb Best flying ancilla combination. + * @param bestPbComb Best pass-by combination. + * @return Chosen mapping method. + */ + [[nodiscard]] MappingMethod + compareShuttlingAndFlyingAncilla(const MoveComb& bestMoveComb, + const FlyingAncillaComb& bestFaComb, + const PassByComb& bestPbComb) const; // Helper methods /** - * @brief Distinguishes between two-qubit swaps and multi-qubit swaps. - * @details Two-qubit swaps only need to swap next to each other, while - * multi-qubit swaps need to swap exactly to the multi-qubit gate position. - * The multi-qubit swaps are weighted depending on their importance to finish - * the multi-qubit gate. - * @param layer The layer to distinguish the swaps for (front or lookahead) - * @return The two-qubit swaps and multi-qubit swaps for the given layer + * @brief Classify swaps into close-by (2-qubit) vs. exact (multi-qubit + * positioning). + * @param layer Gate list (front or lookahead). + * @return Pair (close-by swaps, weighted exact swaps). */ std::pair initSwaps(const GateList& layer); /** - * @brief Helper function to set the two-qubit swap weight to the minimal - * weight of all multi-qubit gates, or 1. - * @param swapExact The exact moves from multi-qubit gates + * @brief Set base two-qubit swap weight to min multi-qubit exact weight or 1. + * @param swapExact Weighted exact swaps. */ void setTwoQubitSwapWeight(const WeightedSwaps& swapExact); /** - * @brief Returns the best position for the given gate coordinates. - * @details Recursively calls getMovePositionRec - * @param gateCoords The coordinates of the gate to find the best position for - * @return The best position for the given gate coordinates + * @brief Compute best coordinate aggregation for gate qubits. + * @param gateCoords Current coordinate indices of gate's qubits. + * @return Selected position indices candidate set. */ CoordIndices getBestMovePos(const CoordIndices& gateCoords); MultiQubitMovePos getMovePositionRec(MultiQubitMovePos currentPos, const CoordIndices& gateCoords, const size_t& maxNMoves); /** - * @brief Returns possible move combinations to move the gate qubits to the - * given position. - * @param gateQubits The gate qubit to be moved - * @param position The target position of the gate qubits - * @return Possible move combinations to move the gate qubits to the given - * position + * @brief Enumerate move combinations relocating gate qubits to target + * coordinates. + * @param gateQubits Hardware qubits participating in gate. + * @param position Target coordinate set. + * @return List of move combination candidates. */ - MoveCombs getMoveCombinationsToPosition(HwQubits& gateQubits, - CoordIndices& position); + [[nodiscard]] MoveCombs + getMoveCombinationsToPosition(const HwQubits& gateQubits, + const CoordIndices& position) const; // Multi-qubit gate based methods /** - * @brief Returns the best position for the given multi-qubit gate. - * @details Calls getBestMultiQubitPositionRec to find the best position by - * performing a recursive search in a breadth-first manner. - * @param opPointer The multi-qubit gate to find the best position for - * @return The best position for the given multi-qubit gate + * @brief Determine optimal convergence position for a multi-qubit gate. + * @param opPointer Multi-qubit operation. + * @return Hardware qubit set representing chosen position. */ HwQubits getBestMultiQubitPosition(const qc::Operation* opPointer); + /** + * @brief Recursive helper to search for optimal multi-qubit convergence + * position. + * @param remainingGateQubits Remaining gate participants. + * @param selectedQubits Already selected qubits (path state). + * @param remainingNearbyQubits Candidate nearby qubits. + * @return Selected hardware qubit set. + */ HwQubits getBestMultiQubitPositionRec(HwQubits remainingGateQubits, std::vector selectedQubits, HwQubits remainingNearbyQubits); /** - * @brief Returns the swaps needed to move the given qubits to the given - * multi-qubit gate position. - * @param op The multi-qubit gate to find the best position for - * @param position The target position of the multi-qubit gate - * @return The swaps needed to move the given qubits to the given multi-qubit + * @brief Compute exact swaps required to align qubits to target multi-qubit + * position. + * @param op Multi-qubit operation. + * @param position Target hardware qubit set. + * @return Weighted swap list for alignment. */ WeightedSwaps getExactSwapsToPosition(const qc::Operation* op, HwQubits position); // Cost function calculation /** - * @brief Calculates the distance reduction for a swap gate given the - * necessary close by swaps and exact moves. - * @details Close by swaps are from two qubit gates, which only require to - * swap close by. The exact moves are from multi-qubit gates, that require - * swapping exactly to the multi-qubit gate position. - * @param swap The swap gate to compute the distance reduction for - * @param swapCloseBy The close by swaps from two-qubit gates - * @param moveExact The exact moves from multi-qubit gates - * @return The distance reduction cost + * @brief Compute distance reduction contribution for a swap. + * @param swap Candidate swap. + * @param swapCloseBy Close-by (2-qubit) swaps. + * @param swapExact Weighted exact swaps (multi-qubit alignment). + * @return Reduction metric (higher means better improvement). */ qc::fp swapCostPerLayer(const Swap& swap, const Swaps& swapCloseBy, const WeightedSwaps& swapExact); /** - * @brief Calculates the cost of a swap gate. - * @details The cost of a swap gate is computed with the following terms: - * - distance reduction for front + lookahead layers using swapCostPerLayer - * - decay term for blocked qubit from last swaps - * The cost is negative. - * @param swap The swap gate to compute the cost for - * @return The cost of the swap gate + * @brief Aggregate total cost for a swap (distance reduction + decay + * penalties). + * @param swap Candidate swap. + * @param swapsFront Pair (close-by, exact) for front layer. + * @param swapsLookahead Pair (close-by, exact) for lookahead layer. + * @return Total cost (lower preferred if negative convention, or compared + * relatively). */ qc::fp swapCost(const Swap& swap, const std::pair& swapsFront, const std::pair& swapsLookahead); /** - * @brief Calculates the cost of a move operation. - * @details Assumes the move is executed and computes the distance reduction - * for the layer. - * @param move The move operation to compute the cost for - * @param layer The layer to compute the distance reduction for - * @return The distance reduction cost + * @brief Distance reduction from a move combination for a given layer. + * @param moveComb Move combo. + * @param layer Target layer. + * @return Distance reduction score. */ - qc::fp moveCostPerLayer(const AtomMove& move, GateList& layer); - + [[nodiscard]] qc::fp moveCombDistanceReduction(const MoveComb& moveComb, + const GateList& layer) const; /** - * @brief Calculates a parallelization cost if the move operation can be - * parallelized with the last moves. - * @param move The move operation to compute the cost for - * @return The parallelization cost + * @brief Distance reduction from a swap for a given layer. + * @param swap Swap candidate. + * @param layer Target layer. + * @return Distance reduction score. */ - qc::fp parallelMoveCost(const AtomMove& move); + qc::fp swapDistanceReduction(const Swap& swap, const GateList& layer); + /** - * @brief Calculates the cost of a move operation. - * @details The cost of a move operation is computed with the following terms: - * - distance reduction for front + lookahead layers using moveCostPerLayer - * - parallelization term based on last moves using parallelMoveCost - * The three contributions are weighted with the runtime parameters. - * @param move The move operation to compute the cost for - * @return The cost of the move operation + * @brief Compute bonus/penalty for parallelizing a move with recent moves. + * @param moveComb Move combination candidate. + * @return Parallelization cost component. */ - qc::fp moveCost(const AtomMove& move); + [[nodiscard]] qc::fp parallelMoveCost(const MoveComb& moveComb) const; /** - * @brief Calculates the cost of a series of move operations by summing up the - * cost of each move. - * @param moveComb The series of move operations to compute the cost for - * @return The total cost of the series of move operations + * @brief Total cost for a move combination (distance reduction + + * parallelization). + * @param moveComb Candidate combination. + * @return Aggregate cost value. */ - qc::fp moveCostComb(const MoveComb& moveComb); + [[nodiscard]] qc::fp moveCostComb(const MoveComb& moveComb) const; /** - * @brief Print the current layers for debugging. + * @brief Debug print of current front/lookahead layers. */ - void printLayers(); + void printLayers() const; public: // Constructors - [[maybe_unused]] NeutralAtomMapper(const NeutralAtomMapper&) = delete; - NeutralAtomMapper& operator=(const NeutralAtomMapper&) = delete; - NeutralAtomMapper(NeutralAtomMapper&&) = delete; + NeutralAtomMapper() = delete; + explicit NeutralAtomMapper(const NeutralAtomArchitecture* architecture, + const MapperParameters& p) + : arch(architecture), scheduler(*architecture), parameters(p), + hardwareQubits(*arch, arch->getNqubits() - p.numFlyingAncillas, + p.initialCoordMapping, p.seed), + flyingAncillas(*arch, p.numFlyingAncillas, Trivial, p.seed) { + const auto nPositions = static_cast(arch->getNpositions()); + const auto nQubits = static_cast(arch->getNqubits()); + if (nPositions - nQubits < 1 && p.shuttlingWeight > 0) { + throw std::runtime_error( + "No free coordinates for shuttling but shuttling " + "weight is greater than 0."); + } + if (parameters.numFlyingAncillas > 1) { + throw std::runtime_error("Only one flying ancilla is supported for now."); + } + // precompute exponential decay weights + decayWeights.reserve(arch->getNcolumns()); + for (uint32_t i = arch->getNcolumns(); i > 0; --i) { + decayWeights.emplace_back(std::exp(-parameters.decay * i)); + } + } explicit NeutralAtomMapper(const NeutralAtomArchitecture& architecture, const MapperParameters& p = MapperParameters()) - : arch(architecture), mappedQc(architecture.getNpositions()), - mappedQcAOD(architecture.getNpositions()), scheduler(architecture), - parameters(p), hardwareQubits(architecture, parameters.initialMapping, - parameters.seed) { - // need at least on free coordinate to shuttle - if (architecture.getNpositions() - architecture.getNqubits() < 1) { - this->parameters.gateWeight = 1; - this->parameters.shuttlingWeight = 0; - } - }; + : NeutralAtomMapper(&architecture, p) {} /** - * @brief Sets the runtime parameters of the mapper. - * @param p The runtime parameters of the mapper + * @brief Set/replace runtime parameters and reset internal state. + * @param p New parameter set. + * @throw std::runtime_error If shuttling weight >0 but no free coordinates or + * unsupported number of flying ancillas. */ void setParameters(const MapperParameters& p) { - this->parameters = p; - if (arch.getNpositions() - arch.getNqubits() < 1) { - this->parameters.gateWeight = 1; - this->parameters.shuttlingWeight = 0; + parameters = p; + const auto nPositions = static_cast(arch->getNpositions()); + const auto nQubits = static_cast(arch->getNqubits()); + if (nPositions - nQubits < 1 && p.shuttlingWeight > 0) { + throw std::runtime_error( + "No free coordinates for shuttling but shuttling " + "weight is greater than 0."); } - this->reset(); + if (parameters.numFlyingAncillas > 1) { + throw std::runtime_error("Only one flying ancilla is supported for now."); + } + reset(); } /** - * @brief Resets the mapper and the hardware qubits. + * @brief Shallow copy of architecture, parameters, mapping, placement and + * scheduler state. + * @param mapper Source mapper. + */ + void copyStateFrom(const NeutralAtomMapper& mapper) { + arch = mapper.arch; + parameters = mapper.parameters; + mapping = mapper.mapping; + hardwareQubits = mapper.hardwareQubits; + lastMoves = mapper.lastMoves; + lastBlockedQubits = mapper.lastBlockedQubits; + scheduler = mapper.scheduler; + decayWeights = mapper.decayWeights; + flyingAncillas = mapper.flyingAncillas; + } + + /** + * @brief Reset mapping and hardware placement (reinitialize qubits & + * ancillas). */ void reset() { hardwareQubits = - HardwareQubits(arch, parameters.initialMapping, parameters.seed); + HardwareQubits(*arch, arch->getNqubits() - parameters.numFlyingAncillas, + parameters.initialCoordMapping, parameters.seed); + flyingAncillas = HardwareQubits(*arch, parameters.numFlyingAncillas, + Trivial, parameters.seed); } // Methods @@ -420,115 +543,156 @@ class NeutralAtomMapper { * @param qc The quantum circuit to be mapped * @param initialMapping The initial mapping of the circuit qubits to the * hardware qubits - * @param verbose If true, prints additional information * @return The mapped quantum circuit with abstract SWAP gates and MOVE * operations */ qc::QuantumComputation map(qc::QuantumComputation& qc, - InitialMapping initialMapping); + const Mapping& initialMapping) { + stats = MapperStats(); + mappedQc = qc::QuantumComputation(arch->getNpositions()); + mappedQcAOD = qc::QuantumComputation(arch->getNpositions()); + mapAppend(qc, initialMapping); + return mappedQc; + } + + qc::QuantumComputation map(qc::QuantumComputation& qc, + const InitialMapping initialMapping) { + const auto actualMapping = + Mapping(qc.getNqubits(), initialMapping, qc, hardwareQubits); + return map(qc, actualMapping); + } /** - * @brief Maps the given quantum circuit to the given architecture and - * converts it to the AOD level. - * @param qc The quantum circuit to be mapped - * @param initialMapping The initial mapping of the circuit qubits to the - * hardware qubits + * @brief Append and map additional circuit portion using given initial + * mapping. + * @param qc Circuit to extend mapping with. + * @param initialMapping Initial mapping state. */ - [[maybe_unused]] void mapAndConvert(qc::QuantumComputation& qc, - InitialMapping initialMapping, - bool printInfo) { - this->parameters.verbose = printInfo; - map(qc, initialMapping); - convertToAod(this->mappedQc); + void mapAppend(qc::QuantumComputation& qc, const Mapping& initialMapping); + + /** + * @brief Retrieve accumulated mapping statistics. + * @return Stats struct. + */ + [[nodiscard]] MapperStats getStats() const { return stats; } + + [[maybe_unused]] [[nodiscard]] std::unordered_map + getStatsMap() const { + std::unordered_map result; + result["nSwaps"] = stats.nSwaps; + result["nBridges"] = stats.nBridges; + result["nFAncillas"] = stats.nFAncillas; + result["nMoves"] = stats.nMoves; + result["nPassBy"] = stats.nPassBy; + return result; } /** - * @brief Prints the mapped circuits as an extended OpenQASM string. - * @return The mapped quantum circuit with abstract SWAP gates and MOVE + * @brief Get current mapped circuit (abstract operations form). + * @return Mapped circuit object. + */ + [[nodiscard]] const qc::QuantumComputation& getMappedQc() const { + return mappedQc; + } + + /** + * @brief Serialize mapped circuit (abstract operations) to extended OpenQASM. + * @return OpenQASM string. */ - [[maybe_unused]] std::string getMappedQc() { + [[nodiscard]] [[maybe_unused]] std::string getMappedQcQasm() const { std::stringstream ss; - this->mappedQc.dumpOpenQASM(ss, false); + mappedQc.dumpOpenQASM(ss, false); return ss.str(); } /** - * @brief Saves the mapped quantum circuit to a file. - * @param filename The name of the file to save the mapped quantum circuit to + * @brief Save mapped abstract circuit (SWAP/MOVE) to file in OpenQASM. + * @param filename Output file path. */ - [[maybe_unused]] void saveMappedQc(const std::string& filename) { + [[maybe_unused]] void saveMappedQcQasm(const std::string& filename) const { std::ofstream ofs(filename); - this->mappedQc.dumpOpenQASM(ofs, false); + mappedQc.dumpOpenQASM(ofs, false); } /** - * @brief Prints the mapped circuit with AOD operations as an extended - * OpenQASM - * @return The mapped quantum circuit with native AOD operations + * @brief Get mapped circuit serialized at native AOD (movement + CZ) level. + * @return OpenQASM string (AOD-native). */ - [[maybe_unused]] std::string getMappedQcAOD() { + [[maybe_unused]] std::string getMappedQcAodQasm() { + if (mappedQcAOD.empty()) { + convertToAod(); + } std::stringstream ss; - this->mappedQcAOD.dumpOpenQASM(ss, false); + mappedQcAOD.dumpOpenQASM(ss, false); return ss.str(); } /** - * @brief Saves the mapped quantum circuit with AOD operations to a file. - * @param filename The name of the file to save the mapped quantum circuit - * with AOD operations to + * @brief Save AOD-native mapped circuit to file. + * @param filename Output file path. */ - [[maybe_unused]] void saveMappedQcAOD(const std::string& filename) { + [[maybe_unused]] void saveMappedQcAodQasm(const std::string& filename) { + if (mappedQcAOD.empty()) { + convertToAod(); + } std::ofstream ofs(filename); - this->mappedQcAOD.dumpOpenQASM(ofs, false); + if (!ofs) { + throw std::runtime_error("Failed to open file: " + filename); + } + mappedQcAOD.dumpOpenQASM(ofs, false); } /** - * @brief Schedules the mapped quantum circuit on the neutral atom - * architecture. - * @details For each gate/operation in the input circuit, the scheduler checks - * the earliest possible time slot for execution. If the gate is a multi qubit - * gate, also the blocking of other qubits is taken into consideration. The - * execution times are read from the neutral atom architecture. - * @param verboseArg If true, prints additional information - * @param createAnimationCsv If true, creates a csv file for the animation - * @param shuttlingSpeedFactor The factor to speed up the shuttling time - * @return The results of the scheduler + * @brief Schedule mapped circuit (AOD level) on architecture timeline. + * @details Lazily converts to AOD if needed, then computes start times with + * blocking, timing and optional animation. + * @param verboseArg Enable verbose scheduler output. + * @param createAnimationCsv Generate animation CSV artifacts. + * @param shuttlingSpeedFactor Factor to scale shuttling durations. + * @return Scheduler results (timings, animation, metrics). */ [[maybe_unused]] SchedulerResults - schedule(bool verboseArg = false, bool createAnimationCsv = false, - qc::fp shuttlingSpeedFactor = 1.0) { + schedule(const bool verboseArg = false, const bool createAnimationCsv = false, + const qc::fp shuttlingSpeedFactor = 1.0) { + if (mappedQcAOD.empty()) { + convertToAod(); + } return scheduler.schedule(mappedQcAOD, hardwareQubits.getInitHwPos(), - verboseArg, createAnimationCsv, - shuttlingSpeedFactor); + flyingAncillas.getInitHwPos(), verboseArg, + createAnimationCsv, shuttlingSpeedFactor); } /** - * @brief Saves the animation csv file of the scheduled quantum circuit. - * @return The animation csv string + * @brief Retrieve animation CSV content produced by scheduler. + * @return CSV string. */ - [[maybe_unused]] std::string getAnimationCsv() { - return scheduler.getAnimationCsv(); + [[maybe_unused]] [[nodiscard]] std::string getAnimationViz() const { + return scheduler.getAnimationViz(); } /** - * @brief Saves the animation csv file of the scheduled quantum circuit. - * @param filename The name of the file to save the animation csv file to + * @brief Persist animation CSV assets to disk. + * @param filename Base filename for output. */ - [[maybe_unused]] void saveAnimationCsv(const std::string& filename) { - scheduler.saveAnimationCsv(filename); + [[maybe_unused]] void saveAnimationFiles(const std::string& filename) const { + scheduler.saveAnimationFiles(filename); } + void decomposeBridgeGates(qc::QuantumComputation& qc) const; + /** - * @brief Converts a mapped circuit down to the AOD level and CZ level. - * @details SWAP gates are decomposed into CX gates. Then CnX gates are - * decomposed into CnZ gates. Move operations are combined if possible and - * then converted into native AOD operations. - * @param qc The already mapped quantum circuit with abstract SWAP gates and - * MOVE operations - * @return The mapped quantum circuit with native AOD operations + * @brief Convert abstract mapped circuit to native AOD (movement & CZ) level. + * @details Decompose SWAP to CX sequence, multi-controlled X to CZ form, + * merge consecutive MOVE operations, and translate to AOD-native + * instructions. + * @return Converted quantum computation object. */ - qc::QuantumComputation convertToAod(qc::QuantumComputation& qc); + qc::QuantumComputation convertToAod(); + /** + * @brief Initial hardware placement map (delegated from HardwareQubits). + * @return Map: hardware qubit -> initial coordinate index. + */ [[maybe_unused]] [[nodiscard]] std::map getInitHwPos() const { return hardwareQubits.getInitHwPos(); diff --git a/include/hybridmap/HybridSynthesisMapper.hpp b/include/hybridmap/HybridSynthesisMapper.hpp new file mode 100644 index 000000000..600bf86f7 --- /dev/null +++ b/include/hybridmap/HybridSynthesisMapper.hpp @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +// +// This file is part of the MQT QMAP library released under the MIT license. +// See README.md or go to https://github.com/cda-tum/qmap for more information. +// + +#pragma once + +#include "HybridNeutralAtomMapper.hpp" +#include "NeutralAtomArchitecture.hpp" +#include "NeutralAtomUtils.hpp" +#include "hybridmap/NeutralAtomDefinitions.hpp" +#include "ir/Definitions.hpp" +#include "ir/QuantumComputation.hpp" + +#include +#include +#include +#include +#include +#include + +namespace na { + +/** + * @brief Bridges circuit synthesis (e.g., ZX extraction) with neutral-atom + * mapping. + * @details Derived from NeutralAtomMapper, this class maintains device state + * and current mapping while accepting proposed synthesis steps. It evaluates + * steps by mapping effort (e.g., required swaps/shuttling and timing), can + * append steps with or without mapping, and exposes utilities to exchange + * information with the synthesis algorithm. + */ +class HybridSynthesisMapper : public NeutralAtomMapper { + using qcs = std::vector; + + qc::QuantumComputation synthesizedQc; + bool initialized = false; + + /** + * @brief Evaluate a single proposed synthesis step. + * @details Effort considers swaps/shuttling and execution time estimated by + * the mapper. + * @param qc Proposed synthesis subcircuit. + * @return Scalar cost/effort score for mapping qc. + */ + qc::fp evaluateSynthesisStep(qc::QuantumComputation& qc) const; + +public: + // Constructors + HybridSynthesisMapper() = delete; + /** + * @brief Construct with device and optional mapper parameters. + * @param arch Neutral atom architecture. + * @param params Optional mapper configuration parameters. + */ + explicit HybridSynthesisMapper( + const NeutralAtomArchitecture& arch, + const MapperParameters& params = MapperParameters()) + : NeutralAtomMapper(arch, params) {} + + // Functions + + /** + * @brief Initialize synthesized and mapped circuits and mapping structures. + * @param nQubits Number of logical qubits to synthesize. + */ + void initMapping(const size_t nQubits) { + if (nQubits > arch->getNpositions()) { + throw std::runtime_error("Not enough qubits in architecture."); + } + mappedQc = qc::QuantumComputation(arch->getNpositions()); + synthesizedQc = qc::QuantumComputation(nQubits); + mapping = Mapping(nQubits); + initialized = true; + } + + /** + * @brief Complete a (re-)mapping of the synthesized circuit to hardware. + * @param initMapping Initial mapping heuristic (defaults to Identity). + */ + void completeRemap(const InitialMapping initMapping = Identity) { + auto qcCopy = synthesizedQc; + map(qcCopy, initMapping); + } + + /** + * @brief Get the currently synthesized (unmapped) circuit. + * @return Synthesized QuantumComputation. + */ + [[nodiscard]] qc::QuantumComputation getSynthesizedQc() const { + return synthesizedQc; + } + + /** + * @brief Export synthesized circuit as OpenQASM string. + * @return QASM representation of the synthesized circuit. + */ + [[nodiscard]] [[maybe_unused]] std::string getSynthesizedQcQASM() const { + std::stringstream ss; + synthesizedQc.dumpOpenQASM(ss, false); + return ss.str(); + } + + /** + * @brief Save synthesized circuit as OpenQASM to a file. + * @param filename Output filename. + */ + [[maybe_unused]] void saveSynthesizedQc(const std::string& filename) const { + std::ofstream ofs(filename); + synthesizedQc.dumpOpenQASM(ofs, false); + ofs.close(); + } + + /** + * @brief Evaluate candidate synthesis steps and optionally map the best. + * @param synthesisSteps Vector of candidate subcircuits. + * @param alsoMap If true, append and map the best candidate. + * @return List of fidelity scores for mapped steps (order matches input). + */ + std::vector evaluateSynthesisSteps(qcs& synthesisSteps, + bool alsoMap = false); + + /** + * @brief Append gates without mapping (no SWAPs/shuttling inserted). + * @param qc Subcircuit to append as-is. + */ + void appendWithoutMapping(const qc::QuantumComputation& qc); + + /** + * @brief Append and map a subcircuit to hardware (may insert moves/SWAPs). + * @param qc Subcircuit to append and map. + */ + void appendWithMapping(qc::QuantumComputation& qc); + + /** + * @brief Get the current device adjacency (connectivity) matrix. + * @return Symmetric adjacency matrix for the neutral atom hardware. + */ + [[nodiscard]] AdjacencyMatrix getCircuitAdjacencyMatrix() const; +}; +} // namespace na diff --git a/include/hybridmap/Mapping.hpp b/include/hybridmap/Mapping.hpp index caf70f1ee..749bd8702 100644 --- a/include/hybridmap/Mapping.hpp +++ b/include/hybridmap/Mapping.hpp @@ -10,10 +10,14 @@ #pragma once +#include "HardwareQubits.hpp" +#include "NeutralAtomArchitecture.hpp" +#include "circuit_optimizer/CircuitOptimizer.hpp" #include "hybridmap/NeutralAtomDefinitions.hpp" #include "hybridmap/NeutralAtomUtils.hpp" #include "ir/Definitions.hpp" #include "ir/Permutation.hpp" +#include "ir/QuantumComputation.hpp" #include "ir/operations/Operation.hpp" #include @@ -21,71 +25,149 @@ #include #include #include +#include namespace na { /** - * @brief Class to manage the mapping between circuit qubits and hardware qubits - * in a bijective manner. + * @brief Maintains a bijective mapping between circuit (logical) and hardware + * qubits. + * @details Supports different initialization strategies, queries in both + * directions, and in-place rewriting of operation qubit indices. The mapping is + * kept one-to-one; swaps can update the mapping as the circuit is transformed. */ class Mapping { protected: - // std::map + using DAG = std::vector*>>; + qc::Permutation circToHw; + HardwareQubits hwQubits; + DAG dag; + + /** + * @brief Compute an initial mapping via (heuristic) graph matching. + * @details Matches circuit interaction structure to device structure to + * reduce expected routing overhead. + * @return Vector mapping logical qubit index i -> chosen hardware index for + * qubit index i. + */ + [[nodiscard]] + std::vector graphMatching(); public: + /** + * @brief Default-construct an empty mapping. + */ Mapping() = default; - Mapping(size_t nQubits, InitialMapping initialMapping) { + /** + * @brief Initialize with identity mapping for the first nQubits. + * @param nQubits Number of logical qubits; maps i -> i for i in [0,nQubits). + */ + explicit Mapping(const size_t nQubits) { + for (size_t i = 0; i < nQubits; ++i) { + circToHw.emplace(i, i); + } + } + /** + * @brief Construct a mapping using a chosen initialization strategy. + * @param nQubits Number of logical qubits to map. + * @param initialMapping Initialization strategy (Identity or Graph). + * @param qc Circuit used to derive structure for graph-based initialization. + * @param hwQubitsArg Target hardware description (capacity/topology + * constraints). + * @throw std::runtime_error If the circuit has more qubits than available + * hardware qubits. + */ + Mapping(const size_t nQubits, const InitialMapping initialMapping, + qc::QuantumComputation& qc, HardwareQubits hwQubitsArg) + : hwQubits(std::move(hwQubitsArg)), + dag(qc::CircuitOptimizer::constructDAG(qc)) { + + if (qc.getNqubits() > hwQubits.getNumQubits()) { + throw std::runtime_error("Not enough qubits in architecture for circuit"); + } + if (nQubits > qc.getNqubits()) { + throw std::runtime_error( + "nQubits exceeds number of qubits in provided circuit"); + } + switch (initialMapping) { case Identity: for (size_t i = 0; i < nQubits; ++i) { circToHw.emplace(i, i); } break; - default: - qc::unreachable(); + case Graph: + const auto qubitIndices = graphMatching(); + for (size_t i = 0; i < nQubits; i++) { + circToHw.emplace(i, qubitIndices[i]); + } + break; } } /** * @brief Assigns a circuit qubit to a hardware qubit. - * @param qubit The circuit qubit to be assigned - * @param hwQubit The hardware qubit to be assigned + * @param qubit Circuit qubit to assign. + * @param hwQubit Hardware qubit index. */ - void setCircuitQubit(qc::Qubit qubit, HwQubit hwQubit) { + void setCircuitQubit(const qc::Qubit qubit, const HwQubit hwQubit) { circToHw[qubit] = hwQubit; } /** * @brief Returns the hardware qubit assigned to the given circuit qubit. - * @param qubit The circuit qubit to be queried - * @return The hardware qubit assigned to the given circuit qubit + * @param qubit Circuit qubit to query. + * @return Hardware qubit assigned to the given circuit qubit. + * @throw std::out_of_range If the circuit qubit is not present in the + * mapping. */ - [[nodiscard]] HwQubit getHwQubit(qc::Qubit qubit) const { + [[nodiscard]] HwQubit getHwQubit(const qc::Qubit qubit) const { return circToHw.at(qubit); } /** * @brief Returns the hardware qubits assigned to the given circuit qubits. - * @param qubits The circuit qubits to be queried - * @return The hardware qubits assigned to the given circuit qubits + * @param qubits Set of circuit qubits to query. + * @return Set of corresponding hardware qubits. + * @throw std::out_of_range If any circuit qubit is not present in the + * mapping. */ [[nodiscard]] std::set - getHwQubits(std::set& qubits) const { - std::set hwQubits; + getHwQubits(const std::set& qubits) const { + std::set hw; + for (const auto& qubit : qubits) { + hw.emplace(getHwQubit(qubit)); + } + return hw; + } + + /** + * @brief Returns the hardware qubits assigned to the given circuit qubits. + * @param qubits Ordered list of circuit qubits to query. + * @return Vector of corresponding hardware qubits (same order as input). + * @throw std::out_of_range If any circuit qubit is not present in the + * mapping. + */ + [[nodiscard]] std::vector + getHwQubits(const std::vector& qubits) const { + std::vector hw; + hw.reserve(qubits.size()); for (const auto& qubit : qubits) { - hwQubits.emplace(this->getHwQubit(qubit)); + hw.emplace_back(getHwQubit(qubit)); } - return hwQubits; + return hw; } /** * @brief Returns the circuit qubit assigned to the given hardware qubit. * @details Throws an exception if the hardware qubit is not assigned to any * circuit qubit. - * @param qubit The hardware qubit to be queried - * @return The circuit qubit assigned to the given hardware qubit + * @param qubit Hardware qubit to query. + * @return Circuit qubit assigned to the given hardware qubit. + * @throw std::runtime_error If the hardware qubit is not found in the + * mapping. */ - [[nodiscard]] qc::Qubit getCircQubit(HwQubit qubit) const { + [[nodiscard]] qc::Qubit getCircQubit(const HwQubit qubit) const { for (const auto& [circQubit, hwQubit] : circToHw) { if (hwQubit == qubit) { return circQubit; @@ -98,20 +180,21 @@ class Mapping { /** * @brief Indicates if any circuit qubit is assigned to the given hardware * qubit. - * @param qubit The hardware qubit to be queried - * @return True if any circuit qubit is assigned to the given hardware qubit, - * false otherwise + * @param qubit Hardware qubit to query. + * @return True if any circuit qubit currently maps to this hardware qubit; + * false otherwise. */ [[nodiscard]] bool isMapped(HwQubit qubit) const { - return std::any_of( - circToHw.begin(), circToHw.end(), - [qubit](const auto& pair) { return pair.second == qubit; }); + return std::ranges::any_of( + circToHw, [qubit](const auto& pair) { return pair.second == qubit; }); } /** * @brief Converts the qubits of an operation from circuit qubits to hardware * qubits. - * @param op The operation to be converted + * @details Rewrites targets (and controls, if present) in-place using the + * current mapping. + * @param op Operation to be converted (modified in place). */ void mapToHwQubits(qc::Operation* op) const { op->setTargets(circToHw.apply(op->getTargets())); @@ -123,10 +206,11 @@ class Mapping { /** * @brief Interchanges the mapping of two hardware qubits. At least one of it * must be mapped to a circuit qubit. - * @param swap The two circuit qubits to be swapped - * @throws std::runtime_error if hardware qubits are not mapped + * @param swap Pair of hardware qubits whose mapped circuit qubits shall be + * swapped. + * @throw std::runtime_error If neither hardware qubit is currently mapped. */ - void applySwap(Swap swap); + void applySwap(const Swap& swap); }; } // namespace na diff --git a/include/hybridmap/MoveToAodConverter.hpp b/include/hybridmap/MoveToAodConverter.hpp index de9345000..7c36eb275 100644 --- a/include/hybridmap/MoveToAodConverter.hpp +++ b/include/hybridmap/MoveToAodConverter.hpp @@ -10,6 +10,7 @@ #pragma once +#include "hybridmap/HardwareQubits.hpp" #include "hybridmap/NeutralAtomArchitecture.hpp" #include "hybridmap/NeutralAtomDefinitions.hpp" #include "hybridmap/NeutralAtomUtils.hpp" @@ -19,58 +20,84 @@ #include "ir/operations/OpType.hpp" #include "na/entities/Location.hpp" +#include #include +#include #include +#include #include #include namespace na { -// Possible types two Move combination can be combined to +/** + * @brief Result type for merging two move-derived activations. + * @details Indicates whether merging is impossible, trivial, a full merge, or + * requires appending. + */ enum class ActivationMergeType : uint8_t { Impossible, Trivial, Merge, Append }; -// Used to indicate how AodOperations can be merged +/** + * @brief Pair of merge types for X and Y dimensions. + */ using MergeTypeXY = std::pair; /** - * @brief Class to convert abstract move operations to AOD movements on a - * neutral atom architecture - * @details The scheduler takes a quantum circuit containing abstract move - * operations and tries to merge them into parallel AO movements. It also - * manages the small offset movements required while loading or unloading of - * AODs. + * @brief Converts abstract atom moves into concrete AOD activation/move + * sequences. + * @details Groups parallelizable moves, computes safe offset maneuvers for + * loading/unloading AODs, and emits AOD operations (activate, move, deactivate) + * respecting device constraints. */ class MoveToAodConverter { + struct AncillaAtom { + struct XAndY { + std::uint32_t x; + std::uint32_t y; + XAndY(const std::uint32_t xCoord, const std::uint32_t yCoord) + : x(xCoord), y(yCoord) {} + }; + + XAndY coord; + XAndY coordDodged; + XAndY offset; + XAndY offsetDodged; + AncillaAtom() = delete; + AncillaAtom(const XAndY c, const XAndY o) + : coord(c), coordDodged(c), offset(o), offsetDodged(o) {} + }; + using AncillaAtoms = std::vector; + protected: /** - * @brief Struct to store information about specific AOD activations. - * @details It contains: - * - the offset moves in x and y direction - * - the actual moves + * @brief Helper for constructing and merging AOD activations/moves. + * @details Tracks per-dimension AOD moves with offsets and associated atom + * moves; produces AodOperation sequences for activation/move/deactivation. */ struct AodActivationHelper { /** - * @brief Describes a single AOD movement in either x or y direction - * @details It contains: - * - the initial position of the AOD - * - the delta of the AOD movement - * - the size of the offset of the AOD movement + * @brief Single AOD movement in one dimension (x or y). + * @details Stores initial position, movement delta, required offset to + * avoid crossing, and whether a load/unload is needed. */ struct AodMove { // start of the move uint32_t init; + // need load/unload or not + bool load; // need offset move to avoid crossing int32_t offset; // delta of the actual move qc::fp delta; - AodMove() = default; - - AodMove(uint32_t initMove, qc::fp deltaMove, int32_t offsetMove) - : init(initMove), offset(offsetMove), delta(deltaMove) {} + AodMove(const uint32_t initMove, const qc::fp deltaMove, + const int32_t offsetMove, const bool loadMove) + : init(initMove), load(loadMove), offset(offsetMove), + delta(deltaMove) {} }; /** - * @brief Manages the activation of an atom using an AOD. - * @details The same struct is used also to deactivate the AOD, just - * reversed. + * @brief Aggregate of per-dimension activation moves plus logical atom + * move. + * @details Represents either activation or deactivation depending on + * context. Holds x- and y- dimension AOD moves and the associated AtomMove. */ struct AodActivation { // first: x, second: delta x, third: offset x @@ -95,7 +122,7 @@ class MoveToAodConverter { } [[nodiscard]] std::vector> - getActivates(Dimension dim) const { + getActivates(const Dimension dim) const { if (dim == Dimension::X) { return activateXs; } @@ -103,103 +130,107 @@ class MoveToAodConverter { } }; - // AODScheduler // NeutralAtomArchitecture to call necessary hardware information - const NeutralAtomArchitecture& arch; + const NeutralAtomArchitecture* arch; std::vector allActivations; // Differentiate between loading and unloading qc::OpType type; + AncillaAtoms* ancillas; // Constructor AodActivationHelper() = delete; AodActivationHelper(const AodActivationHelper&) = delete; AodActivationHelper(AodActivationHelper&&) = delete; AodActivationHelper(const NeutralAtomArchitecture& architecture, - qc::OpType opType) - : arch(architecture), type(opType) {} + const qc::OpType opType, AncillaAtoms* ancillas) + : arch(&architecture), type(opType), ancillas(ancillas) {} // Methods /** - * @brief Returns all AOD moves in the given dimension/direction which start - * at the given initial position - * @param dim The dimension/direction to check - * @param init The initial position to check - * @return A vector of AOD moves + * @brief Return all AOD moves along a dimension that start at a given + * position. + * @param dim Dimension (X or Y). + * @param init Initial position index. + * @return Vector of matching AOD move descriptors. */ [[nodiscard]] std::vector> getAodMovesFromInit(Dimension dim, uint32_t init) const; // Activation management /** - * @brief Adds the move to the current activations - * @details The move is merged into the current activations depending on the - * given merge types - * @param merge The merge types in x and y direction - * @param origin The origin of the move - * @param move The move to add - * @param v The move vector of the move + * @brief Merge an atom move into current activations according to merge + * policy. + * @details Uses per-dimension merge types to either merge, append, or + * reject combining with in-flight activations; records offsets and + * load/unload handling. + * @param merge Merge policy for X and Y. + * @param origin Origin location. + * @param move Atom move descriptor. + * @param v Geometric move vector. + * @param needLoad Whether an AOD load is required. */ - void - addActivation(std::pair merge, - const Location& origin, const AtomMove& move, MoveVector v); + void addActivation( + const std::pair& merge, + const Location& origin, const AtomMove& move, const MoveVector& v, + bool needLoad); + + void addActivationFa(const Location& origin, const AtomMove& move, + const MoveVector& v, bool needLoad); /** - * @brief Merges the given activation into the current activations - * @param dim The dimension/direction of the activation - * @param activationDim The activation to merge in the given - * dimension/direction - * @param activationOtherDim The activation to merge/add in the other - * dimension/direction + * @brief Merge an activation into the aggregate along a specific dimension. + * @param dim Dimension of the activation. + * @param activationDim Activation to merge for the specified dimension. + * @param activationOtherDim Complementary activation for the other + * dimension. */ void mergeActivationDim(Dimension dim, const AodActivation& activationDim, const AodActivation& activationOtherDim); /** - * @brief Orders the aod offset moves such that they will not cross each - * other - * @param aodMoves The aod offset moves to order - * @param sign The direction of the offset moves (right/left or down/up) + * @brief Reorder offset moves to avoid crossing. + * @param aodMoves Collection of offset moves to reorder. + * @param sign Direction of offsets (+1/-1 for right/left or down/up). */ static void reAssignOffsets(std::vector>& aodMoves, int32_t sign); /** - * @brief Returns the maximum offset in the given dimension/direction from - * the given initial position - * @param dim The dimension/direction to check - * @param init The initial position to check - * @param sign The direction of the offset moves (right/left or down/up) - * @return The maximum offset + * @brief Maximum absolute offset at a position along a dimension. + * @param dim Dimension. + * @param init Initial position. + * @param sign Direction (+1/-1). + * @return Maximum offset value. */ [[nodiscard]] uint32_t getMaxOffsetAtInit(Dimension dim, uint32_t init, int32_t sign) const; /** - * @brief Checks if there is still space at the given initial position and - * the given direction - * @param dim The dimension/direction to check - * @param init The initial position to check - * @param sign The direction of the offset moves (right/left or down/up) - * @return True if there is still space, false otherwise + * @brief Check whether additional offset space is available at a position. + * @param dim Dimension. + * @param init Initial position. + * @param sign Direction (+1/-1). + * @return True if more offset steps fit; false otherwise. */ [[nodiscard]] bool checkIntermediateSpaceAtInit(Dimension dim, uint32_t init, int32_t sign) const; + void computeInitAndOffsetOperations( + Dimension dimension, const std::shared_ptr& aodMove, + std::vector& initOperations, + std::vector& offsetOperations) const; // Convert activation to AOD operations /** - * @brief Converts activation into AOD operation (activate, move, - * deactivate) - * @param activation The activation to convert - * @param arch The neutral atom architecture to call necessary hardware - * information - * @param type The type of the activation (loading or unloading) - * @return The activation as AOD operation + * @brief Convert a single activation aggregate into AOD operations. + * @details Emission order: activate, move, deactivate. + * @param activation Activation aggregate to convert. + * @return Vector of emitted AOD operations. */ - [[nodiscard]] std::pair + [[nodiscard]] std::vector getAodOperation(const AodActivation& activation) const; /** - * @brief Converts all activations into AOD operations - * @return All activations of the AOD activation helper as AOD operations + * @brief Convert all stored activations into AOD operations. + * @return Concatenated vector of emitted AOD operations. */ [[nodiscard]] std::vector getAodOperations() const; }; @@ -222,6 +253,7 @@ class MoveToAodConverter { // the moves and the index they appear in the original quantum circuit (to // insert them back later) std::vector> moves; + std::vector> movesFa; std::vector processedOpsInit; std::vector processedOpsFinal; AodOperation processedOpShuttle; @@ -232,80 +264,115 @@ class MoveToAodConverter { // Methods /** - * @brief Checks if the given move can be added to the move group - * @param move Move to check - * @return True if the move can be added, false otherwise + * @brief Check if a move can be added to the current group. + * @param move Move to check. + * @param archArg Architecture for geometric/constraint checks. + * @return True if compatible with group; false otherwise. */ - bool canAdd(const AtomMove& move, const NeutralAtomArchitecture& archArg); + bool canAddMove(const AtomMove& move, + const NeutralAtomArchitecture& archArg); /** - * @brief Adds the given move to the move group - * @param move Move to add - * @param idx Index of the move in the original quantum circuit + * @brief Add a move to the group. + * @param move Move to add. + * @param idx Circuit index of the move. */ - void add(const AtomMove& move, uint32_t idx); + void addMove(const AtomMove& move, uint32_t idx); /** - * @brief Returns the circuit index of the first move in the move group - * @return Circuit index of the first move in the move group + * @brief Circuit index of the earliest move in the group. + * @return Minimum circuit index across stored moves. */ - [[nodiscard]] uint32_t getFirstIdx() const { return moves.front().second; } + + [[nodiscard]] uint32_t getFirstIdx() const { + assert(!moves.empty() || !movesFa.empty()); + if (moves.empty()) { + return movesFa.front().second; + } + if (movesFa.empty()) { + return moves.front().second; + } + return std::min(moves.front().second, movesFa.front().second); + } /** - * @brief Checks if the two moves can be executed in parallel - * @param v1 The first move - * @param v2 The second move - * @return True if the moves can be executed in parallel, false otherwise + * @brief Check if two moves are parallelizable. + * @param v1 First move vector. + * @param v2 Second move vector. + * @return True if they can execute in parallel; false otherwise. */ static bool parallelCheck(const MoveVector& v1, const MoveVector& v2); /** - * @brief Helper function to create the actual shuttling operation between - * the loading at the initial position and the unloading at the final - * position - * @param opsInit Loading operations - * @param opsFinal Unloading operations - * @return The shuttling operation between the loading and unloading - * operations + * @brief Build the shuttling operation connecting load and unload phases. + * @param aodActivationHelper Activation helper (loading phase info). + * @param aodDeactivationHelper Deactivation helper (unloading phase info). + * @return Constructed AOD shuttling operation. */ static AodOperation - connectAodOperations(const std::vector& opsInit, - const std::vector& opsFinal); + connectAodOperations(const AodActivationHelper& aodActivationHelper, + const AodActivationHelper& aodDeactivationHelper); }; const NeutralAtomArchitecture& arch; qc::QuantumComputation qcScheduled; std::vector moveGroups; + const HardwareQubits& hardwareQubits; + AncillaAtoms ancillas; + + AtomMove convertOpToMove(qc::Operation* get) const; + + void initFlyingAncillas(); /** - * @brief Assigns move operations into groups that can be executed in parallel - * @param qc Quantum circuit to schedule + * @brief Partition moves into groups that can execute in parallel. + * @param qc Quantum circuit to schedule. */ - void initMoveGroups(qc::QuantumComputation& qc); + void initMoveGroups( + qc::QuantumComputation& qc); //, qc::Permutation& hwToCoordIdx); /** - * @brief Converts the move groups into the actual AOD operations - * @details For this the following steps are performed: - * - ActivationHelper to manage the loading - * - ActivationHelper to manage the unloading - * If not the whole move group can be executed in parallel, a new move group - * is created for the remaining moves. + * @brief Convert move groups into concrete AOD operations. + * @details Uses activation/deactivation helpers to emit load/move/unload + * sequences; splits groups when parallelism constraints require it. */ void processMoveGroups(); + std::pair, MoveGroup> + processMoves(const std::vector>& moves, + AodActivationHelper& aodActivationHelper, + AodActivationHelper& aodDeactivationHelper) const; + void processMovesFa(const std::vector>& movesFa, + AodActivationHelper& aodActivationHelper, + AodActivationHelper& aodDeactivationHelper) const; + public: MoveToAodConverter() = delete; MoveToAodConverter(const MoveToAodConverter&) = delete; MoveToAodConverter(MoveToAodConverter&&) = delete; - explicit MoveToAodConverter(const NeutralAtomArchitecture& archArg) - : arch(archArg), qcScheduled(arch.getNpositions()) {} + explicit MoveToAodConverter(const NeutralAtomArchitecture& archArg, + const HardwareQubits& hardwareQubitsArg, + const HardwareQubits& flyingAncillas) + : arch(archArg), qcScheduled(arch.getNpositions()), + hardwareQubits(hardwareQubitsArg) { + qcScheduled.addAncillaryRegister(arch.getNpositions()); + qcScheduled.addAncillaryRegister(arch.getNpositions(), "fa"); + for (std::uint32_t i = 0; i < flyingAncillas.getInitHwPos().size(); ++i) { + const auto coord = + flyingAncillas.getInitHwPos().at(i) + (2 * arch.getNpositions()); + const auto col = coord % arch.getNcolumns(); + const auto row = coord / arch.getNcolumns(); + const AncillaAtom ancillaAtom({col, row}, {i + 1, i + 1}); + ancillas.emplace_back(ancillaAtom); + } + } /** - * @brief Schedules the given quantum circuit using AODs - * @param qc Quantum circuit to schedule - * @return Scheduled quantum circuit, containing AOD operations + * @brief Schedule a circuit: replace abstract moves by AOD load/move/unload. + * @param qc Quantum circuit to schedule. + * @return New circuit containing AOD operations. */ qc::QuantumComputation schedule(qc::QuantumComputation& qc); /** - * @brief Returns the number of move groups - * @return Number of move groups + * @brief Get number of constructed move groups. + * @return Count of move groups. */ [[nodiscard]] auto getNMoveGroups() const { return moveGroups.size(); } }; diff --git a/include/hybridmap/NeutralAtomArchitecture.hpp b/include/hybridmap/NeutralAtomArchitecture.hpp index 48d49a021..3941b5583 100644 --- a/include/hybridmap/NeutralAtomArchitecture.hpp +++ b/include/hybridmap/NeutralAtomArchitecture.hpp @@ -14,51 +14,42 @@ #include "hybridmap/NeutralAtomDefinitions.hpp" #include "hybridmap/NeutralAtomUtils.hpp" #include "ir/Definitions.hpp" +#include "ir/QuantumComputation.hpp" #include "ir/operations/OpType.hpp" +#include "ir/operations/Operation.hpp" #include "na/entities/Location.hpp" +#include +#include +#include #include #include #include #include +#include #include #include +#include +#include #include +#include #include namespace na { /** - * @brief Class to store the properties of a neutral atom architecture - * @details - * The properties of a neutral atom architecture are: - * - number of rows - * - number of columns - * - number of AODs - * - number of AOD coordinates - * - inter-qubit distance - * - interaction radius - * - blocking factor - * - minimal AOD distance - * The properties are loaded from a JSON file. - * - * The class also provides functions to compute the swap distances between - * qubits and the nearby qubits for each qubit. + * @brief Device model for a neutral atom architecture. + * @details Holds fixed device properties and run-time parameters loaded from + * JSON, derives coordinate layout, connectivity (swap distances), and proximity + * lists. Provides timing and fidelity queries, distance helpers, and optional + * animation export. + * It requires at last one default "none" entry in gate times and fidelities. */ class NeutralAtomArchitecture { /** - * @brief Class to store the properties of a neutral atom architecture - * @details - * The properties of a neutral atom architecture are: - * - number of rows - * - number of columns - * - number of AODs - * - number of AOD coordinates - * - inter-qubit distance - * - interaction radius - * - blocking factor - * - minimal AOD distance - * The properties are loaded from a JSON file and are fixed for each - * architecture. + * @brief Fixed, immutable properties of a device. + * @details Encodes grid layout and geometry: rows/columns, number of AODs and + * coordinates, inter-qubit distance, interaction radius, blocking factor, and + * derived AOD intermediate levels. */ class Properties { protected: @@ -72,64 +63,101 @@ class NeutralAtomArchitecture { qc::fp blockingFactor; public: + /** + * @brief Default-construct with zeroed properties. + */ Properties() = default; - Properties(std::uint16_t rows, std::uint16_t columns, std::uint16_t aods, - std::uint16_t aodCoordinates, qc::fp qubitDistance, - qc::fp radius, qc::fp blockingFac, qc::fp aodDist) + /** + * @brief Construct with explicit device properties. + * @param rows Grid rows. + * @param columns Grid columns. + * @param aods Number of AODs. + * @param aodCoordinates Number of AOD coordinates. + * @param qubitDistance Inter-qubit spacing (grid unit). + * @param radius Interaction radius (in grid units or scaled distance). + * @param blockingFac Blocking factor for concurrent operations. + * @param aodDist Minimum AOD step distance used to derive intermediate + * levels. + */ + Properties(const std::uint16_t rows, const std::uint16_t columns, + const std::uint16_t aods, const std::uint16_t aodCoordinates, + const qc::fp qubitDistance, const qc::fp radius, + const qc::fp blockingFac, const qc::fp aodDist) : nRows(rows), nColumns(columns), nAods(aods), - nAodIntermediateLevels( - static_cast(qubitDistance / aodDist)), nAodCoordinates(aodCoordinates), interQubitDistance(qubitDistance), - interactionRadius(radius), blockingFactor(blockingFac) {} + interactionRadius(radius), blockingFactor(blockingFac) { + assert(aodDist > 0); + nAodIntermediateLevels = static_cast(qubitDistance / aodDist); + assert(nAodIntermediateLevels >= 1); + } + /** + * @brief Total grid sites (rows*columns). + * @return Number of positions. + */ [[nodiscard]] std::uint16_t getNpositions() const { return nRows * nColumns; } + /** + * @brief Grid rows. + */ [[nodiscard]] std::uint16_t getNrows() const { return nRows; } + /** + * @brief Grid columns. + */ [[nodiscard]] std::uint16_t getNcolumns() const { return nColumns; } + /** + * @brief Number of AODs. + */ [[nodiscard]] std::uint16_t getNAods() const { return nAods; } + /** + * @brief Number of AOD coordinates. + */ [[nodiscard]] std::uint16_t getNAodCoordinates() const { return nAodCoordinates; } + /** + * @brief Number of intermediate AOD steps between neighboring sites. + */ [[nodiscard]] std::uint16_t getNAodIntermediateLevels() const { return nAodIntermediateLevels; } + /** + * @brief Inter-qubit spacing. + */ [[nodiscard]] qc::fp getInterQubitDistance() const { return interQubitDistance; } + /** + * @brief Interaction radius. + */ [[nodiscard]] qc::fp getInteractionRadius() const { return interactionRadius; } + /** + * @brief Blocking factor. + */ [[nodiscard]] qc::fp getBlockingFactor() const { return blockingFactor; } }; /** - * @brief Class to store the parameters of a neutral atom architecture - * @details - * The parameters of a neutral atom architecture are: - * - number of qubits - * - gate times - * - gate average fidelities - * - shuttling times - * - shuttling average fidelities - * - decoherence times - * The parameters are loaded from a JSON file. - * The difference to the properties is that the parameters can change - * from run to run. + * @brief Run-time parameters of a device (may vary per run). + * @details Includes number of active qubits, gate and shuttling + * times/fidelities, and decoherence times loaded from JSON. */ struct Parameters { /** - * @brief Struct to store the decoherence times of a neutral atom - * architecture - * @details - * The decoherence times of a neutral atom architecture are: - * - T1 [µs] - * - T2 [µs] - * - effective decoherence time [µs] + * @brief Longitudinal and transverse decoherence times. + * @details Provides an effective time tEff = (T1*T2)/(T1+T2) when both are + * non-zero. */ struct DecoherenceTimes { qc::fp t1 = 0; qc::fp t2 = 0; + /** + * @brief Effective decoherence time. + * @return 0 if both T1 and T2 are zero; otherwise (T1*T2)/(T1+T2). + */ [[nodiscard]] qc::fp tEff() const { if (t1 == 0 && t2 == 0) { return 0; @@ -137,7 +165,7 @@ class NeutralAtomArchitecture { return t1 * t2 / (t1 + t2); } }; - CoordIndex nQubits; + CoordIndex nQubits = 0; std::map gateTimes; std::map gateAverageFidelities; std::map shuttlingTimes; @@ -153,25 +181,24 @@ class NeutralAtomArchitecture { qc::SymmetricMatrix swapDistances; std::vector> nearbyCoordinates; + // Bridges only makes sense for short distances (3-9) so we limit its size + BridgeCircuits bridgeCircuits = BridgeCircuits(10); + /** - * @brief Create the coordinates. + * @brief Create grid coordinates for each position on the device. */ void createCoordinates(); /** - * @brief Compute the swap distances between the coordinates - * @details - * The swap distances are computed using the coordinates of the qubits. - * The swap distance is the distance between the qubits in terms of - * edges in the resulting connectivity graph. This can be computed - * beforehand. + * @brief Precompute swap distances between coordinates. + * @details Build connectivity graph based on interaction radius and compute + * shortest-path distances in number of edges. + * @param interactionRadius Interaction radius used to define connectivity. */ void computeSwapDistances(qc::fp interactionRadius); /** - * @brief Compute the nearby coordinates for each coordinate - * @details - * The nearby qubits are the qubits that are close enough to be connected - * by an edge in the resulting connectivity graph. This can be be computed - * beforehand. + * @brief Precompute per-site lists of nearby coordinates. + * @details Nearby coordinates are those within interaction radius forming + * edges in the connectivity graph. */ void computeNearbyCoordinates(); @@ -179,183 +206,196 @@ class NeutralAtomArchitecture { std::string name; /** - * @brief Construct a new Neutral Atom Architecture object - * @details - * The properties of the architecture are loaded from a JSON file. - * @param filename The name of the JSON file + * @brief Construct an architecture by loading its JSON description. + * @param filename Path to the JSON file. + * @details Loads properties and parameters, then derives coordinates, + * connectivity, and proximity tables. + * @throw std::runtime_error If the file cannot be opened or parsed (depending + * on JSON loader implementation). */ explicit NeutralAtomArchitecture(const std::string& filename); /** - * @brief Load the properties of the architecture from a JSON file - * @param filename The name of the JSON file + * @brief Load (or reload) properties and parameters from JSON. + * @param filename Path to the JSON file. + * @throw std::runtime_error If the file cannot be opened or parsed (depending + * on JSON loader implementation). */ void loadJson(const std::string& filename); // Getters /** - * @brief Get the number of rows - * @return The number of rows + * @brief Get the number of rows. + * @return Row count. */ [[nodiscard]] std::uint16_t getNrows() const { return properties.getNrows(); } /** - * @brief Get the number of columns - * @return The number of columns + * @brief Get the number of columns. + * @return Column count. */ [[nodiscard]] std::uint16_t getNcolumns() const { return properties.getNcolumns(); } /** - * @brief Get the number of positions - * @return The number of positions + * @brief Get the number of grid positions. + * @return Positions (rows*columns). */ [[nodiscard]] std::uint16_t getNpositions() const { return properties.getNpositions(); } /** - * @brief Get the number of AODs - * @return The number of AODs + * @brief Get the number of AODs. + * @return AOD count. */ [[maybe_unused]] [[nodiscard]] std::uint16_t getNAods() const { return properties.getNAods(); } /** - * @brief Get the number of AOD coordinates - * @return The number of AOD coordinates + * @brief Get the number of AOD coordinates. + * @return AOD coordinate count. */ [[nodiscard]] [[maybe_unused]] std::uint16_t getNAodCoordinates() const { return properties.getNAodCoordinates(); } /** - * @brief Get the number of qubits - * @return The number of qubits + * @brief Get the number of qubits. + * @return Active qubit count. */ [[nodiscard]] CoordIndex getNqubits() const { return parameters.nQubits; } /** - * @brief Get the inter-qubit distance - * @return The inter-qubit distance + * @brief Get the inter-qubit distance. + * @return Spacing between neighboring sites. */ [[nodiscard]] qc::fp getInterQubitDistance() const { return properties.getInterQubitDistance(); } + /** + * @brief Distance represented by one AOD intermediate level. + * @return Inter-qubit distance divided by number of AOD intermediate levels. + */ + [[nodiscard]] qc::fp getOffsetDistance() const { + return getInterQubitDistance() / getNAodIntermediateLevels(); + } /** - * @brief Get the interaction radius - * @return The interaction radius + * @brief Get the interaction radius. + * @return Interaction radius. */ [[nodiscard]] qc::fp getInteractionRadius() const { return properties.getInteractionRadius(); } /** - * @brief Get the blocking factor - * @return The blocking factor + * @brief Get the blocking factor. + * @return Blocking factor. */ [[nodiscard]] qc::fp getBlockingFactor() const { return properties.getBlockingFactor(); } /** - * @brief Get precomputed swap distance between two coordinates - * @param idx1 The index of the first coordinate - * @param idx2 The index of the second coordinate - * @return The swap distance between the two coordinates + * @brief Get precomputed swap distance between two coordinates. + * @param idx1 First coordinate index. + * @param idx2 Second coordinate index. + * @return Swap distance (#edges) between the coordinates. */ - [[nodiscard]] SwapDistance getSwapDistance(CoordIndex idx1, - CoordIndex idx2) const { + [[nodiscard]] SwapDistance getSwapDistance(const CoordIndex idx1, + const CoordIndex idx2) const { return swapDistances(idx1, idx2); } /** - * @brief Get precomputed swap distance between two coordinates - * @param c1 The first coordinate - * @param c2 The second coordinate - * @return The swap distance between the two coordinates + * @brief Get precomputed swap distance between two coordinates. + * @param c1 First coordinate. + * @param c2 Second coordinate. + * @return Swap distance (#edges) between the coordinates. */ [[nodiscard]] SwapDistance getSwapDistance(const Location& c1, const Location& c2) const { return swapDistances( - static_cast(c1.x + c1.y) * properties.getNcolumns(), - static_cast(c2.x + c2.y) * properties.getNcolumns()); + static_cast(c1.x + (c1.y * properties.getNcolumns())), + static_cast(c2.x + (c2.y * properties.getNcolumns()))); } /** - * @brief Get the number of AOD intermediate levels, i.e. the number of - * possible positions between two coordinates. - * @return The number of AOD intermediate levels + * @brief Number of AOD intermediate levels (positions between two neighbors). + * @return AOD intermediate levels. */ [[nodiscard]] uint16_t getNAodIntermediateLevels() const { return properties.getNAodIntermediateLevels(); } /** - * @brief Get the execution time of an operation - * @param op The operation - * @return The execution time of the operation + * @brief Get the execution time of an operation. + * @param op Operation pointer. + * @return Duration of the operation on this device. */ [[nodiscard]] qc::fp getOpTime(const qc::Operation* op) const; /** - * @brief Get the fidelity of an operation - * @param op The operation - * @return The fidelity of the operation + * @brief Get the average fidelity of an operation. + * @param op Operation pointer. + * @return Average fidelity for the operation on this device. */ [[nodiscard]] qc::fp getOpFidelity(const qc::Operation* op) const; /** - * @brief Get indices of the nearby coordinates that are blocked by an - * operation - * @param op The operation - * @return The indices of the nearby coordinates that are blocked by the - * operation + * @brief Get indices of nearby coordinates blocked by an operation. + * @param op Operation pointer. + * @return Set of coordinate indices blocked while executing op. */ [[nodiscard]] std::set getBlockedCoordIndices(const qc::Operation* op) const; // Getters for the parameters + /** + * @brief Retrieve the base time for a named gate. + * @param s Gate name. + * @return Gate time if present; otherwise falls back to the time of "none" + * and prints a message. + * @note If the fallback entry "none" is not present, an exception from + * std::map::at may be thrown. + */ [[nodiscard]] qc::fp getGateTime(const std::string& s) const { - if (parameters.gateTimes.find(s) == parameters.gateTimes.end()) { - std::cout << "Gate time for " << s << " not found\n" - << "Returning default value\n"; + if (!parameters.gateTimes.contains(s)) { + SPDLOG_WARN("Gate time for '{}' not found. Returning default value.", s); return parameters.gateTimes.at("none"); } return parameters.gateTimes.at(s); } /** - * @brief Retrieves the average fidelity of a gate. - * - * This function is responsible for fetching the average fidelity of a gate - * specified by its name. If the gate is not found in the parameters, it will - * print a message to the console and return the average fidelity of a default - * gate. - * - * @param s The name of the gate. - * @return The average fidelity of the specified gate or the default gate if - * the specified gate is not found. + * @brief Retrieve the average fidelity for a named gate. + * @param s Gate name. + * @return Gate average fidelity if present; otherwise falls back to the + * fidelity of "none" and prints a message. + * @note If the fallback entry "none" is not present, an exception from + * std::map::at may be thrown. */ [[nodiscard]] qc::fp getGateAverageFidelity(const std::string& s) const { - if (parameters.gateAverageFidelities.find(s) == - parameters.gateAverageFidelities.end()) { - std::cout << "Gate average fidelity for " << s << " not found\n" - << "Returning default value\n"; + if (!parameters.gateAverageFidelities.contains(s)) { + SPDLOG_WARN( + "Gate average fidelity for '{}' not found. Returning default value.", + s); return parameters.gateAverageFidelities.at("none"); } return parameters.gateAverageFidelities.at(s); } /** - * @brief Get the shuttling time of a shuttling operation - * @param shuttlingType The type of the shuttling operation - * @return The shuttling time of the shuttling operation + * @brief Get the shuttling time of an operation type. + * @param shuttlingType Shuttling operation type (OpType). + * @return Shuttling time for the given type. + * @throw std::out_of_range If the operation type is unknown. */ - [[nodiscard]] qc::fp getShuttlingTime(qc::OpType shuttlingType) const { + [[nodiscard]] qc::fp getShuttlingTime(const qc::OpType shuttlingType) const { return parameters.shuttlingTimes.at(shuttlingType); } /** - * @brief Get the average fidelity of a shuttling operation - * @param shuttlingType The type of the shuttling operation - * @return The average fidelity of the shuttling operation + * @brief Get the average fidelity of a shuttling operation type. + * @param shuttlingType Shuttling operation type (OpType). + * @return Average shuttling fidelity for the given type. + * @throw std::out_of_range If the operation type is unknown. */ [[nodiscard]] qc::fp - getShuttlingAverageFidelity(qc::OpType shuttlingType) const { + getShuttlingAverageFidelity(const qc::OpType shuttlingType) const { return parameters.shuttlingAverageFidelities.at(shuttlingType); } /** - * @brief Get the decoherence time - * @return The decoherence time + * @brief Get the effective decoherence time of the device. + * @return Effective T (computed from T1 and T2). */ [[nodiscard]] qc::fp getDecoherenceTime() const { return parameters.decoherenceTimes.tEff(); @@ -363,60 +403,131 @@ class NeutralAtomArchitecture { // Converters between indices and coordinates /** - * @brief Get a coordinate corresponding to an index - * @param idx The index - * @return The coordinate corresponding to the index + * @brief Convert an index to a grid coordinate. + * @param idx Coordinate index. + * @return Location at the given index. */ - [[nodiscard]] Location getCoordinate(CoordIndex idx) const { + [[nodiscard]] Location getCoordinate(const CoordIndex idx) const { return coordinates[idx]; } /** - * @brief Get the index corresponding to a coordinate - * @param c The coordinate - * @return The index corresponding to the coordinate + * @brief Convert a grid coordinate to its index. + * @param c Location. + * @return Linearized index for the coordinate. + */ + [[nodiscard]] [[maybe_unused]] CoordIndex getIndex(const Location& c) const { + return static_cast(c.x + (c.y * properties.getNcolumns())); + } + + /** + * @brief Retrieve a precomputed bridge circuit of a given chain length. + * @param length Linear chain length. + * @return QuantumComputation holding the bridge circuit. */ - [[nodiscard]] [[maybe_unused]] CoordIndex getIndex(const Location& c) { - return static_cast(c.x + c.y * properties.getNcolumns()); + [[nodiscard]] [[maybe_unused]] qc::QuantumComputation + getBridgeCircuit(const size_t length) const { + assert(length < bridgeCircuits.bridgeCircuits.size()); + return bridgeCircuits.bridgeCircuits[length]; } // Distance functions /** - * @brief Get the Euclidean distance between two coordinate indices - * @param idx1 The index of the first coordinate - * @param idx2 The index of the second coordinate - * @return The Euclidean distance between the two coordinate indices + * @brief Euclidean distance between two coordinate indices. + * @param idx1 First coordinate index. + * @param idx2 Second coordinate index. + * @return Euclidean distance. */ [[nodiscard]] qc::fp getEuclideanDistance(const CoordIndex idx1, const CoordIndex idx2) const { return coordinates.at(idx1).getEuclideanDistance(coordinates.at(idx2)); } /** - * @brief Get the Euclidean distance between two coordinates - * @param c1 The first coordinate - * @param c2 The second coordinate - * @return The Euclidean distance between the two coordinates + * @brief Sum of pairwise Euclidean distances among a set of indices. + * @param coords Set of coordinate indices. + * @return Sum of distances across ordered pairs (i!=j). + */ + [[nodiscard]] qc::fp + getAllToAllEuclideanDistance(const std::set& coords) const { + qc::fp dist = 0; + for (auto const c1 : coords) { + for (auto const c2 : coords) { + if (c1 == c2) { + continue; + } + dist += getEuclideanDistance(c1, c2); + } + } + return dist; + } + /** + * @brief Total Euclidean distance covered by an aggregate move combination. + * @param moveComb Combination of atom moves. + * @return Sum of Euclidean distances for each move. + */ + [[nodiscard]] qc::fp + getMoveCombEuclideanDistance(const MoveComb& moveComb) const { + qc::fp dist = 0; + for (const auto& move : moveComb.moves) { + dist += getEuclideanDistance(move.c1, move.c2); + } + return dist; + } + + /** + * @brief Estimated path length for a flying-ancilla combination. + * @param faComb Flying ancilla move combination. + * @return Sum of distances along origin->q1 and twice q1->q2 per step. + */ + [[nodiscard]] qc::fp + getFaEuclideanDistance(const FlyingAncillaComb& faComb) const { + qc::fp dist = 0; + for (const auto& fa : faComb.moves) { + dist += getEuclideanDistance(fa.origin, fa.q1); + dist += getEuclideanDistance(fa.q1, fa.q2) * 2; + } + return dist; + } + + /** + * @brief Estimated path length for a pass-by combination. + * @param pbComb Pass-by move combination. + * @return Twice the sum of distances of its constituent moves. + */ + [[nodiscard]] qc::fp + getPassByEuclideanDistance(const PassByComb& pbComb) const { + qc::fp dist = 0; + for (const auto& fa : pbComb.moves) { + dist += getEuclideanDistance(fa.c1, fa.c2) * 2; + } + return dist; + } + + /** + * @brief Euclidean distance between two coordinates. + * @param c1 First coordinate. + * @param c2 Second coordinate. + * @return Euclidean distance. */ [[nodiscard]] static qc::fp getEuclideanDistance(const Location& c1, const Location& c2) { return c1.getEuclideanDistance(c2); } /** - * @brief Get the Manhattan distance between two coordinate indices - * @param idx1 The index of the first coordinate - * @param idx2 The index of the second coordinate - * @return The Manhattan distance between the two coordinate indices + * @brief Manhattan distance in X between two indices. + * @param idx1 First index. + * @param idx2 Second index. + * @return |x1-x2|. */ [[nodiscard]] CoordIndex getManhattanDistanceX(const CoordIndex idx1, const CoordIndex idx2) const { return static_cast( - this->coordinates.at(idx1).getManhattanDistanceX( - this->coordinates.at(idx2))); + coordinates.at(idx1).getManhattanDistanceX(coordinates.at(idx2))); } /** - * @brief Get the Manhattan distance between two coordinate indices - * @param idx1 The index of the first coordinate - * @param idx2 The index of the second coordinate - * @return The Manhattan distance between the two coordinate indices + * @brief Manhattan distance in Y between two indices. + * @param idx1 First index. + * @param idx2 Second index. + * @return |y1-y2|. */ [[nodiscard]] CoordIndex getManhattanDistanceY(const CoordIndex idx1, const CoordIndex idx2) const { @@ -426,66 +537,62 @@ class NeutralAtomArchitecture { // Nearby coordinates /** - * @brief Get the precomputed nearby coordinates for a coordinate index - * @param idx The index of the coordinate - * @return The precomputed nearby coordinates for the coordinate index + * @brief Get precomputed nearby coordinates for an index. + * @param idx Coordinate index. + * @return Set of indices within interaction radius of idx. */ [[nodiscard]] std::set getNearbyCoordinates(const CoordIndex idx) const { return nearbyCoordinates[idx]; } /** - * @brief Get the coordinates which are exactly one step away from a - * coordinate index, i.e. the ones above, below, left and right. - * @param idx The index of the coordinate - * @return The coordinates which are exactly one step away from the - * coordinate index + * @brief Get coordinates exactly one grid step away (von Neumann + * neighborhood). + * @param idx Coordinate index. + * @return Indices of neighbors above, below, left, and right. */ [[nodiscard]] std::vector getNN(CoordIndex idx) const; // MoveVector functions /** - * @brief Get the MoveVector between two coordinate indices - * @param idx1 The index of the first coordinate - * @param idx2 The index of the second coordinate - * @return The MoveVector between the two coordinate indices + * @brief Construct a MoveVector between two coordinate indices. + * @param idx1 Start index. + * @param idx2 End index. + * @return MoveVector from start to end. */ - [[nodiscard]] MoveVector getVector(CoordIndex idx1, CoordIndex idx2) const { - return {this->coordinates[idx1].x, this->coordinates[idx1].y, - this->coordinates[idx2].x, this->coordinates[idx2].y}; + [[nodiscard]] MoveVector getVector(const CoordIndex idx1, + const CoordIndex idx2) const { + return {coordinates[idx1].x, coordinates[idx1].y, coordinates[idx2].x, + coordinates[idx2].y}; } /** - * @brief Computes the time it takes to move a qubit along a MoveVector - * @param v The MoveVector - * @return The time it takes to move a qubit along the MoveVector + * @brief Estimate time to move along a MoveVector. + * @param v MoveVector path. + * @return Shuttling time proportional to Euclidean length and device speed. */ [[nodiscard]] qc::fp getVectorShuttlingTime(const MoveVector& v) const { - return v.getLength() * this->getInterQubitDistance() / - this->getShuttlingTime(qc::OpType::Move); + return v.getLength() * getInterQubitDistance() / + getShuttlingTime(qc::OpType::Move); } /** - * @brief Returns a csv string for the animation of the architecture - * @return The csv string for the animation of the architecture + * @brief Generate CSV content describing the device for animation. + * @param shuttlingSpeedFactor Scaling factor applied to shuttling speeds. + * @return CSV string describing layout and timing parameters. */ - [[nodiscard]] std::string getAnimationCsv() const { - std::string csv = "x;y;size;color\n"; - for (auto i = 0; i < getNcolumns(); i++) { - for (auto j = 0; j < getNrows(); j++) { - csv += std::to_string(i * getInterQubitDistance()) + ";" + - std::to_string(j * getInterQubitDistance()) + ";1;2\n"; - } - } - return csv; - } + [[nodiscard]] std::string + getAnimationMachine(qc::fp shuttlingSpeedFactor) const; /** - * @brief Save the animation of the architecture to a csv file - * @param filename The name of the csv file + * @brief Save the device animation CSV to a file. + * @param filename Output CSV filename. + * @param shuttlingSpeedFactor Scaling factor applied to shuttling speeds. */ - [[maybe_unused]] void saveAnimationCsv(const std::string& filename) const { + [[maybe_unused]] void + saveAnimationMachine(const std::string& filename, + const qc::fp shuttlingSpeedFactor) const { std::ofstream file(filename); - file << getAnimationCsv(); + file << getAnimationMachine(shuttlingSpeedFactor); } }; diff --git a/include/hybridmap/NeutralAtomDefinitions.hpp b/include/hybridmap/NeutralAtomDefinitions.hpp index c6f69936f..8703bfbd3 100644 --- a/include/hybridmap/NeutralAtomDefinitions.hpp +++ b/include/hybridmap/NeutralAtomDefinitions.hpp @@ -10,10 +10,13 @@ #pragma once +#include "datastructures/SymmetricMatrix.hpp" #include "ir/Definitions.hpp" +#include "ir/operations/Operation.hpp" #include #include +#include #include #include @@ -23,30 +26,96 @@ class Operation; } // namespace qc namespace na { -// A CoordIndex corresponds to node in the SLM grid, where an atom can be placed -// (or not). +/** + * @brief Index of a site in the SLM grid where an atom may reside. + * @details Refers to a physical coordinate slot; occupancy varies during + * mapping/shuttling. + */ using CoordIndex = std::uint32_t; using CoordIndices = std::vector; -// A HwQubit corresponds to an atom in the neutral atom architecture. It can be -// used as qubit or not and occupies a certain position in the architecture. +using AdjacencyMatrix = qc::SymmetricMatrix; + +/** + * @brief Identifier of an atom (hardware qubit) in the architecture. + * @details May or may not currently host a logical qubit; linked to a + * coordinate index. + */ using HwQubit = uint32_t; using HwQubits = std::set; -using HwPositions [[maybe_unused]] = std::vector; -// A qc::Qubit corresponds to a qubit in the quantum circuit. It can be mapped -// to a hardware qubit. +using HwQubitsVector = std::vector; +/** + * @brief Bridge operation and the sequence of hardware qubits it spans. + * @details The vector lists qubits involved in mediating an interaction (e.g., + * for a CZ bridge chain). + */ +using Bridge = std::pair>; +using Bridges = std::vector; +using HwPositions [[maybe_unused]] = + std::vector; // Deprecated alias for historical layout snapshots. +/** + * @brief Set of logical circuit qubits (indices in the quantum computation). + */ using Qubits = std::set; -// Swaps are between hardware qc::Qubits (one of them can be unmapped). +/** + * @brief Hardware-level SWAP operand pair (one endpoint can be unmapped). + */ using Swap = std::pair; using Swaps = std::vector; +/** + * @brief SWAP annotated with a weight (e.g., cost or heuristic score). + */ using WeightedSwap = std::pair; using WeightedSwaps = std::vector; -// The distance between two hardware qubits using SWAP gates. +/** + * @brief Distance (# of SWAPs) between hardware qubits under current mapping. + */ using SwapDistance = int32_t; -// Moves are between coordinates (the first is occupied, the second is not). -using AtomMove = std::pair; +/** + * @brief Atom shuttle between two coordinate indices. + * @details c1: source (expected occupied), c2: destination (expected free). + * load1/load2 specify load/unload actions (e.g., addressing focus). + */ +struct AtomMove { + CoordIndex c1 = 0; + CoordIndex c2 = 0; + bool load1 = true; + bool load2 = true; + + /** + * @brief Equality comparison. + * @param other Move to compare. + * @return True if all fields match. + */ + bool operator==(const AtomMove& other) const { + return c1 == other.c1 && c2 == other.c2 && load1 == other.load1 && + load2 == other.load2; + } + /** + * @brief Inequality comparison. + * @param other Move to compare. + * @return True if any field differs. + */ + bool operator!=(const AtomMove& other) const { return !(*this == other); } -// Used to represent operations + /** + * @brief Less-than comparison for ordering. + * @param other Move to compare. + * @return True if this move is less than the other in lexicographical order. + */ + bool operator<(const AtomMove& other) const { + return std::tie(c1, c2, load1, load2) < + std::tie(other.c1, other.c2, other.load1, other.load2); + } +}; + +/** + * @brief List of quantum operations (gate pointers), e.g., a layer. + */ using GateList = std::vector; +/** + * @brief Collection of gate lists (e.g., per-qubit candidate sets). + */ +using GateLists = std::vector; } // namespace na diff --git a/include/hybridmap/NeutralAtomLayer.hpp b/include/hybridmap/NeutralAtomLayer.hpp index 301517c0a..3555925a5 100644 --- a/include/hybridmap/NeutralAtomLayer.hpp +++ b/include/hybridmap/NeutralAtomLayer.hpp @@ -22,11 +22,12 @@ namespace na { /** - * @brief Class to manage the creation of layers when traversing a quantum - * circuit. - * @details The class uses the qc::DAG of the circuit to create layers of gates - * that can be executed at the same time. It can be used to create the front or - * look ahead layer. + * @brief Helper for constructing executable or look-ahead layers from a circuit + * DAG. + * @details Consumes a per-qubit DAG representation and maintains frontier + * iterators to build either (a) the front layer of mutually commuting gates + * (ready to execute) or (b) look-ahead layers containing a bounded depth of + * forthcoming multi-qubit gates per qubit for heuristic evaluation. */ class NeutralAtomLayer { @@ -37,76 +38,115 @@ class NeutralAtomLayer { DAG dag; DAGIterators iterators; + DAGIterators ends; GateList gates; - GateList mappedSingleQubitGates; - std::vector candidates; + GateList newGates; + GateLists candidates; + uint32_t lookaheadDepth; + bool isFrontLayer; /** - * @brief Updates the gates for the given qubits - * @details The function iterates over the qc::DAG and updates the gates for - * the given qubits as far es possible. - * @param qubitsToUpdate The qubits that have been updated - * @param commuteWith Gates the new gates should commute with + * @brief Advance frontier and refresh candidates/gates for specified qubits. + * @details Moves DAG iterators forward for each qubit, replenishes candidate + * queues and promotes ready operations into the current layer (all involved + * qubits agree). Front-layer mode restricts to commuting operations; + * look-ahead mode gathers up to lookaheadDepth multi-qubit gates. + * @param qubitsToUpdate Logical qubits whose DAG frontier and candidate sets + * are updated. */ void updateByQubits(const std::set& qubitsToUpdate); + /** - * @brief Updates the candidates for the given qubits + * @brief Extend per-qubit candidate queues from DAG columns. + * @details Front-layer: continue pulling while new ops commute with existing + * layer and per-qubit candidates. Look-ahead: pull until lookaheadDepth + * multi-qubit ops encountered (including intervening single-qubit ops). + * @param qubitsToUpdate Logical qubits whose candidate queues should be + * extended. */ void updateCandidatesByQubits(const std::set& qubitsToUpdate); /** - * @brief Checks the candidates and add them to the gates if possible - * @param qubitsToUpdate The qubits that have been updated + * @brief Promote multi-qubit candidates that are ready on all their qubits. + * @details Checks for each qubit whether the head candidate appears across + * candidate lists of all its operand qubits; if so, moves it to the layer and + * records it in newGates, removing from all candidate lists. + * @param qubitsToUpdate Logical qubits whose candidate lists are evaluated + * for promotion. */ void candidatesToGates(const std::set& qubitsToUpdate); - // Commutation checks - static bool commutesWithAtQubit(const GateList& layer, - const qc::Operation* opPointer, - const qc::Qubit& qubit); - static bool commuteAtQubit(const qc::Operation* opPointer1, - const qc::Operation* opPointer2, - const qc::Qubit& qubit); - public: - // Constructor - explicit NeutralAtomLayer(DAG graph) : dag(std::move(graph)) { + /** + * @brief Construct a layer builder over a per-qubit DAG. + * @param graph Per-qubit DAG columns (each deque owns operation pointers). + * @param isFrontLayer True for executable front layer mode; false for + * look-ahead mode. + * @param lookaheadDepth Max number of multi-qubit gates to look ahead per + * qubit. + */ + explicit NeutralAtomLayer(DAG graph, const bool isFrontLayer, + const uint32_t lookaheadDepth = 1) + : dag(std::move(graph)), lookaheadDepth(lookaheadDepth), + isFrontLayer(isFrontLayer) { iterators.reserve(dag.size()); candidates.reserve(dag.size()); for (auto& i : dag) { auto it = i.begin(); iterators.emplace_back(it); + ends.emplace_back(i.end()); candidates.emplace_back(); } } /** - * @brief Returns the current layer of gates - * @return The current layer of gates + * @brief Get the current executable/look-ahead layer gate list. + * @return Copy of current layer gates. */ - GateList getGates() { return gates; } + [[nodiscard]] GateList getGates() const { return gates; } /** - * @brief Returns a vector of the iterator indices - * @return A copy of the current iterator indices + * @brief Get gates newly added during the latest update. + * @details Populated by the most recent update invocation then refreshed each + * subsequent update. + * @return Copy of gates added since prior update. */ - std::vector getIteratorOffset(); + [[nodiscard]] GateList getNewGates() const { return newGates; } /** - * @brief Initializes the layer by updating all qubits starting from the - * iterators - * @param The iterator offset to start from + * @brief Initialize internal frontiers and populate initial candidates/gates. + * @details Advances all qubit DAG iterators, builds candidate queues and + * promotes ready operations. */ - void initLayerOffset(const std::vector& iteratorOffset = {}); + void initAllQubits(); /** - * @brief Removes the provided gates from the current layer and update the - * the layer depending on the qubits of the gates. - * @param gatesToRemove Gates to remove from the current layer - * @param commuteWith Gates the new gates should commute with + * @brief Remove specified gates then advance affected qubit frontiers. + * @details After erasing gates, updates candidates/gates for all qubits they + * touched, potentially adding new ready operations. + * @param gatesToRemove Gates to erase from current layer. */ void removeGatesAndUpdate(const GateList& gatesToRemove); - /** - * @brief Returns the mapped single qubit gates - * @return The mapped single qubit gates - */ - GateList getMappedSingleQubitGates() { return mappedSingleQubitGates; } }; +// Commutation checks +/** + * @brief Determine if an operation commutes with all layer operations at a + * qubit. + * @param layer Current layer gate list. + * @param opPointer Operation to test. + * @param qubit Qubit index for commutation assessment. + * @return True if opPointer commutes with every gate in layer at qubit; false + * otherwise. + */ +bool commutesWithAtQubit(const GateList& layer, const qc::Operation* opPointer, + const qc::Qubit& qubit); +/** + * @brief Check pairwise commutation of two operations at a qubit. + * @details Simple syntactic rules: non-unitaries never commute; identities and + * single-qubit ops commute; ops not acting on the qubit commute; specific + * two-qubit control/target patterns are considered commuting. + * @param op1 First operation. + * @param op2 Second operation. + * @param qubit Qubit index for commutation assessment. + * @return True if operations commute at qubit; false otherwise. + */ +bool commuteAtQubit(const qc::Operation* op1, const qc::Operation* op2, + const qc::Qubit& qubit); } // namespace na diff --git a/include/hybridmap/NeutralAtomScheduler.hpp b/include/hybridmap/NeutralAtomScheduler.hpp index e72f3bef6..a68ea8fbb 100644 --- a/include/hybridmap/NeutralAtomScheduler.hpp +++ b/include/hybridmap/NeutralAtomScheduler.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -27,7 +28,11 @@ namespace na { /** - * @brief Struct to store the results of the scheduler + * @brief Aggregated metrics produced by the neutral atom scheduler. + * @details Captures timing and fidelity information over the entire scheduled + * circuit: total execution (makespan), accumulated idle time, raw gate fidelity + * product (excluding decoherence), overall fidelity (including idle + * decoherence), and counts of selected operation types. */ struct SchedulerResults { qc::fp totalExecutionTime; @@ -35,26 +40,46 @@ struct SchedulerResults { qc::fp totalGateFidelities; qc::fp totalFidelities; uint32_t nCZs = 0; + uint32_t nAodActivate = 0; + uint32_t nAodMove = 0; - SchedulerResults(qc::fp executionTime, qc::fp idleTime, qc::fp gateFidelities, - qc::fp fidelities, uint32_t cZs) + /** + * @brief Construct and initialize scheduler result metrics. + * @param executionTime Overall makespan (end time of last operation). + * @param idleTime Sum of idle time across qubits: end_time * n_qubits - + * total_gate_time. + * @param gateFidelities Product of native gate fidelities (excluding + * decoherence). + * @param fidelities Overall fidelity including idle-time decoherence. + * @param cZs Number of CZ operations. + * @param aodActivate Number of AOD activation operations. + * @param aodMove Number of AOD shuttling/move operations. + */ + SchedulerResults(const qc::fp executionTime, const qc::fp idleTime, + const qc::fp gateFidelities, const qc::fp fidelities, + const uint32_t cZs, const uint32_t aodActivate, + const uint32_t aodMove) : totalExecutionTime(executionTime), totalIdleTime(idleTime), totalGateFidelities(gateFidelities), totalFidelities(fidelities), - nCZs(cZs) {} + nCZs(cZs), nAodActivate(aodActivate), nAodMove(aodMove) {} - [[nodiscard]] std::string toString() const { - std::stringstream ss; - ss << "Total execution time: " << totalExecutionTime; - ss << "\nTotal idle time: " << totalIdleTime - << "\nTotal fidelities: " << totalFidelities; - return ss.str(); - } + /** + * @brief Export a compact CSV line with execution time, idle time, fidelity. + * @return String formatted as: totalExecutionTime, totalIdleTime, + * totalFidelities + */ [[nodiscard]] std::string toCsv() const { std::stringstream ss; ss << totalExecutionTime << ", " << totalIdleTime << "," << totalFidelities; return ss.str(); } + /** + * @brief Export selected metrics to a key-value map. + * @details Includes totalExecutionTime, totalIdleTime, totalGateFidelities, + * totalFidelities, nCZs, nAodActivate, nAodMove. + * @return Unordered map from metric names to numeric values. + */ [[maybe_unused]] [[nodiscard]] std::unordered_map toMap() const { std::unordered_map result; @@ -63,68 +88,116 @@ struct SchedulerResults { result["totalGateFidelities"] = totalGateFidelities; result["totalFidelities"] = totalFidelities; result["nCZs"] = nCZs; + result["nAodActivate"] = static_cast(nAodActivate); + result["nAodMove"] = static_cast(nAodMove); return result; } }; /** - * @brief Class to schedule a quantum circuit on a neutral atom architecture - * @details For each gate/operation in the input circuit, the scheduler checks - * the earliest possible time slot for execution. If the gate is a multi qubit - * gate, also the blocking of other qubits is taken into consideration. The - * execution times are read from the neutral atom architecture. + * @brief Schedules quantum circuits on a neutral atom architecture. + * @details Iterates operations chronologically assigning earliest feasible + * start times respecting per-gate durations, multi-qubit blocking windows (e.g. + * Rydberg interaction zones), and AOD move/activation timing. Optionally + * records visualization artifacts for animation. */ class NeutralAtomScheduler { protected: - const NeutralAtomArchitecture& arch; - std::string animationCsv; - std::string animationArchitectureCsv; + const NeutralAtomArchitecture* arch; + std::string animation; + std::string animationMachine; public: - // Constructor NeutralAtomScheduler() = delete; - NeutralAtomScheduler(const NeutralAtomScheduler&) = delete; - NeutralAtomScheduler(NeutralAtomScheduler&&) = delete; + /** + * @brief Construct with a given neutral atom architecture. + * @param architecture Architecture reference whose timing data is used. + */ explicit NeutralAtomScheduler(const NeutralAtomArchitecture& architecture) - : arch(architecture) {} + : arch(&architecture) {} /** - * @brief Schedules the given quantum circuit on the neutral atom architecture - * @details For each gate/operation in the input circuit, the scheduler checks - * the earliest possible time slot for execution. If the gate is a multi qubit - * gate, also the blocking of other qubits is taken into consideration. The - * execution times are read from the neutral atom architecture. - * @param qc Quantum circuit to schedule - * @param verbose If true, prints additional information - * @return SchedulerResults + * @brief Schedule a quantum circuit on the architecture. + * @details Greedily assigns earliest feasible start times to each operation + * while tracking per-qubit availability and multi-qubit blocking intervals. + * Generates optional animation traces. + * @param qc Quantum circuit to schedule. + * @param initHwPos Initial atom positions indexed by hardware qubit. + * @param initFaPos Initial AOD focus array positions indexed by hardware + * qubit. + * @param verbose If true, prints progress and summary to stdout. + * @param createAnimationCsv If true, records animation artifacts + * (.naviz/.namachine). + * @param shuttlingSpeedFactor Factor scaling AOD move/activation durations + * (1.0 = unchanged). + * @return SchedulerResults containing makespan, idle time, fidelity metrics, + * and operation counts. */ SchedulerResults schedule(const qc::QuantumComputation& qc, - const std::map& initHwPos, + const std::map& initHwPos, + const std::map& initFaPos, bool verbose, bool createAnimationCsv = false, qc::fp shuttlingSpeedFactor = 1.0); - std::string getAnimationCsv() { return animationCsv; } - void saveAnimationCsv(const std::string& filename) { + /** + * @brief Retrieve machine/layout description (.namachine content). + * @return Machine description string. + * @note Populated only if schedule() ran with createAnimationCsv=true. + */ + [[nodiscard]] std::string getAnimationMachine() const { + return animationMachine; + } + /** + * @brief Retrieve visualization event log (.naviz content). + * @return Event log string. + * @note Populated only if schedule() ran with createAnimationCsv=true. + */ + [[nodiscard]] std::string getAnimationViz() const { return animation; } + + /** + * @brief Write animation artifacts (.naviz/.namachine) to disk. + * @details Uses the stem of the provided filename to derive target paths for + * each artifact. + * @param filename Base filename (its extension is stripped before appending + * artifact extensions). + */ + void saveAnimationFiles(const std::string& filename) const { + if (animation.empty() || animationMachine.empty()) { + SPDLOG_WARN("No animation data to save; did you run schedule() with " + "createAnimationCsv=true?"); + return; + } + const auto filenameWithoutExtension = + filename.substr(0, filename.find_last_of('.')); + const auto filenameViz = filenameWithoutExtension + ".naviz"; + const auto filenameMachine = filenameWithoutExtension + ".namachine"; + // save animation - std::ofstream file(filename); - file << animationCsv; + auto file = std::ofstream(filenameViz); + file << getAnimationViz(); file.close(); - // save architecture - auto filenameWithoutExtension = - filename.substr(0, filename.find_last_of('.')); - file.open(filenameWithoutExtension + "_architecture.csv"); - file << animationArchitectureCsv; + // save machine + file.open(filenameMachine); + file << getAnimationMachine(); file.close(); } // Helper Print functions + /** + * @brief Print a human-readable summary of scheduling results. + * @param totalExecutionTimes Per-qubit cumulative execution times. + * @param totalIdleTime Sum of idle time across all qubits. + * @param totalGateFidelities Product of native gate fidelities. + * @param totalFidelities Overall fidelity including idle-time decoherence. + * @param nCZs Count of CZ gates. + * @param nAodActivate Count of AOD activation operations. + * @param nAodMove Count of AOD move operations. + */ static void printSchedulerResults(std::vector& totalExecutionTimes, qc::fp totalIdleTime, qc::fp totalGateFidelities, - qc::fp totalFidelities, uint32_t nCZs); - static void printTotalExecutionTimes( - std::vector& totalExecutionTimes, - std::vector>>& blockedQubitsTimes); + qc::fp totalFidelities, uint32_t nCZs, + uint32_t nAodActivate, uint32_t nAodMove); }; } // namespace na diff --git a/include/hybridmap/NeutralAtomUtils.hpp b/include/hybridmap/NeutralAtomUtils.hpp index 9cb99d402..f30404418 100644 --- a/include/hybridmap/NeutralAtomUtils.hpp +++ b/include/hybridmap/NeutralAtomUtils.hpp @@ -10,8 +10,10 @@ #pragma once +#include "circuit_optimizer/CircuitOptimizer.hpp" #include "hybridmap/NeutralAtomDefinitions.hpp" #include "ir/Definitions.hpp" +#include "ir/QuantumComputation.hpp" #include "ir/operations/AodOperation.hpp" #include @@ -25,61 +27,138 @@ namespace na { -// Enums for the different initial mappings strategies +// Enums for the different initial mapping strategies +/** + * @brief Strategy for assigning initial physical coordinates to atoms. + * @details "Trivial" assigns coordinates in order without randomness; "Random" + * samples coordinates uniformly from the available lattice/set. + */ enum InitialCoordinateMapping : uint8_t { Trivial, Random }; -enum InitialMapping : uint8_t { Identity }; +/** + * @brief Strategy for initializing the logical-to-physical qubit mapping. + * @details Identity keeps logical indices matched to physical indices; Graph + * performs a topology-aware assignment (e.g., based on interaction graph + * heuristics). + */ +enum InitialMapping : uint8_t { Identity, Graph }; +/** + * @brief Method used to realize two-qubit interactions that are not adjacent + * under the current mapping. + * @details Different strategies trade off added gates vs. atom motion: Swap + * introduces SWAP chains, Bridge builds entangling bridges, Move performs + * physical repositioning, FlyingAncilla uses a mobile ancilla, PassBy leverages + * relative motion without stopping. + */ +enum MappingMethod : uint8_t { + SwapMethod, + BridgeMethod, + MoveMethod, + FlyingAncillaMethod, + PassByMethod +}; +/** + * @brief Parse an InitialCoordinateMapping from a string token. + * @param initialCoordinateMapping Accepted values: "trivial"/"0" or + * "random"/"1" (case-sensitive). + * @return Corresponding InitialCoordinateMapping enum value. + * @throw std::invalid_argument If the value is not recognized. + */ [[maybe_unused]] static InitialCoordinateMapping initialCoordinateMappingFromString( const std::string& initialCoordinateMapping) { if (initialCoordinateMapping == "trivial" || initialCoordinateMapping == "0") { - return InitialCoordinateMapping::Trivial; + return Trivial; } if (initialCoordinateMapping == "random" || initialCoordinateMapping == "1") { - return InitialCoordinateMapping::Random; + return Random; } - throw std::invalid_argument("Invalid initial coordinate mapping value: " + - initialCoordinateMapping); + throw std::invalid_argument( + "Invalid initial coordinate mapping value (expected \"trivial\"/\"0\" " + "or \"random\"/\"1\"): " + + initialCoordinateMapping); } +/** + * @brief Parse an InitialMapping from a string token. + * @param initialMapping Accepted values: "identity"/"0" or "graph"/"1" + * (case-sensitive). + * @return Corresponding InitialMapping enum value. + * @throw std::invalid_argument If the value is not recognized. + */ [[maybe_unused]] static InitialMapping initialMappingFromString(const std::string& initialMapping) { if (initialMapping == "identity" || initialMapping == "0") { - return InitialMapping::Identity; + return Identity; } - throw std::invalid_argument("Invalid initial mapping value: " + - initialMapping); + if (initialMapping == "graph" || initialMapping == "1") { + return Graph; + } + throw std::invalid_argument( + "Invalid initial mapping value (expected \"identity\"/\"0\" or " + "\"graph\"/\"1\"): " + + initialMapping); } /** - * @brief Helper class to represent a direction in x and y coordinates. - * @details The boolean value corresponds to right/left and down/up. + * @brief Helper struct representing a 2D direction via sign bits. + * @details Booleans encode the sign of deltas: x==true implies non-negative x + * movement; y==true implies non-negative y movement per the chosen coordinate + * convention. */ struct Direction { bool x; bool y; - [[maybe_unused]] Direction(bool xDir, bool yDir) : x(xDir), y(yDir) {} - Direction(qc::fp deltaX, qc::fp deltaY) : x(deltaX >= 0), y(deltaY >= 0) {} + /** + * @brief Construct a direction from deltas. + * @param deltaX Signed x delta. + * @param deltaY Signed y delta. + */ + Direction(const qc::fp deltaX, const qc::fp deltaY) + : x(deltaX >= 0), y(deltaY >= 0) {} + /** + * @brief Equality comparison. + * @param other Direction to compare against. + * @return True if both sign bits are identical. + */ [[nodiscard]] bool operator==(const Direction& other) const { return x == other.x && y == other.y; } + /** + * @brief Inequality comparison. + * @param other Direction to compare against. + * @return True if any sign bit differs. + */ [[nodiscard]] bool operator!=(const Direction& other) const { return !(*this == other); } + /** + * @brief Get signed unit for x. + * @return +1 if x>=0 else -1. + */ [[nodiscard]] int32_t getSignX() const { return x ? 1 : -1; } + /** + * @brief Get signed unit for y. + * @return +1 if y>=0 else -1. + */ [[nodiscard]] int32_t getSignY() const { return y ? 1 : -1; } - [[nodiscard]] int32_t getSign(Dimension dim) const { + /** + * @brief Get signed unit for a given dimension. + * @param dim Dimension (X or Y). + * @return +1 if non-negative movement else -1. + */ + [[nodiscard]] int32_t getSign(const Dimension dim) const { return dim == Dimension::X ? getSignX() : getSignY(); } }; /** - * @brief Helper class to represent a move of an atom from one position to - * another. - * @details Each move consists in a start and end coordinate and the direction. + * @brief Represents a single atom move from a start to an end position. + * @details Encapsulates start/end coordinates, derived direction, and spatial + * predicates (direction, length, overlap, inclusion). */ struct MoveVector { qc::fp xStart; @@ -88,90 +167,167 @@ struct MoveVector { qc::fp yEnd; Direction direction; - MoveVector(qc::fp xstart, qc::fp ystart, qc::fp xend, qc::fp yend) - : xStart(xstart), yStart(ystart), xEnd(xend), yEnd(yend), - direction(xend - xstart, yend - ystart) {} - MoveVector(std::int64_t xstart, std::int64_t ystart, std::int64_t xend, - std::int64_t yend) - : xStart(static_cast(xstart)), - yStart(static_cast(ystart)), xEnd(static_cast(xend)), - yEnd(static_cast(yend)), - direction(static_cast(xend - xstart), - static_cast(yend - ystart)) {} + /** + * @brief Construct a move vector. + * @param xStart Starting x coordinate. + * @param yStart Starting y coordinate. + * @param xEnd Ending x coordinate. + * @param yEnd Ending y coordinate. + */ + MoveVector(const qc::fp xStart, const qc::fp yStart, const qc::fp xEnd, + const qc::fp yEnd) + : xStart(xStart), yStart(yStart), xEnd(xEnd), yEnd(yEnd), + direction(xEnd - xStart, yEnd - yStart) {} + /** + * @brief Check if two moves share identical direction signs. + * @param other Other move. + * @return True if both x and y direction signs match. + */ [[nodiscard]] [[maybe_unused]] bool sameDirection(const MoveVector& other) const { return direction == other.direction; } + /** + * @brief Euclidean length of the move. + * @return Hypotenuse of (dx, dy). + */ [[nodiscard]] qc::fp getLength() const { return std::hypot(xEnd - xStart, yEnd - yStart); } + /** + * @brief Check if axis-aligned projections overlap in at least one dimension. + * @param other Other move. + * @return True if x intervals overlap OR y intervals overlap (inclusive + * bounds). + */ [[nodiscard]] bool overlap(const MoveVector& other) const; + /** + * @brief Check if this move's interval is strictly included by another's in + * any dimension. + * @param other Other move that may include this. + * @return True if other strictly contains this in x OR in y. + */ [[nodiscard]] bool include(const MoveVector& other) const; }; /** - * @brief Helper class to manage multiple atom moves which belong together. - * @details E.g. a move-away combined with the actual move. These are combined - * in a MoveComb to facilitate the cost calculation. + * @brief Metadata for a flying ancilla interaction step. + * @details Stores origin and two target data atom coordinates plus an index for + * ordering. + */ +struct FlyingAncilla { + /** Origin coordinate index of the flying ancilla. */ + CoordIndex origin; + /** First data atom coordinate involved in interaction. */ + CoordIndex q1; + /** Second data atom coordinate involved in interaction. */ + CoordIndex q2; + /** Optional bookkeeping index to order/identify moves. */ + size_t index; +}; + +/** + * @brief Combination of sequential flying ancilla moves for one operation. + * @param moves Sequence realizing the interaction. + * @param op Operation implemented (non-owning pointer). + */ +struct FlyingAncillaComb { + /** Sequence of flying ancilla moves realizing an interaction. */ + std::vector moves; + /** Operation this combination implements or is associated with. */ + const qc::Operation* op = nullptr; +}; + +/** + * @brief Pass-by maneuver representation for an operation. + * @details Aggregates physical atom moves needed to realize a pass-by style + * interaction. + */ +struct PassByComb { + /** Sequence of atom moves realizing a pass-by maneuver. */ + std::vector moves; + /** Operation this combination implements or is associated with. */ + const qc::Operation* op = nullptr; +}; + +/** + * @brief Aggregates related atom moves forming a candidate realization. + * @details Examples include preparatory clearance moves plus the main move. + * Collectively evaluated for cost heuristics and selection. */ struct MoveComb { std::vector moves; + /** Aggregated cost heuristic; defaults to +inf until computed. */ qc::fp cost = std::numeric_limits::max(); + /** Operation this move combination aims to realize (optional). */ + const qc::Operation* op = nullptr; + /** Best known positions for the operation after applying the moves. */ + CoordIndices bestPos; - MoveComb(std::vector mov, const qc::fp c) - : moves(std::move(mov)), cost(c) {} - MoveComb(AtomMove mov, const qc::fp c) : moves({std::move(mov)}), cost(c) {} + MoveComb(std::vector mov, const qc::fp c, const qc::Operation* o, + CoordIndices pos) + : moves(std::move(mov)), cost(c), op(o), bestPos(std::move(pos)) {} + MoveComb(AtomMove mov, const qc::fp c, const qc::Operation* o, + CoordIndices pos) + : moves({mov}), cost(c), op(o), bestPos(std::move(pos)) {} MoveComb() = default; explicit MoveComb(std::vector mov) : moves(std::move(mov)) {} - explicit MoveComb(AtomMove mov) : moves({std::move(mov)}) {} + explicit MoveComb(AtomMove mov) : moves({mov}) {} /** - * @brief Get the first move of the combination - * @return The first move of the combination + * @brief Equality comparison (ignores cost/op/bestPos). + * @param other Other combination. + * @return True if underlying move sequence is identical. */ - [[nodiscard]] AtomMove getFirstMove() const { return moves.front(); } - - /** - * @brief Get the last move of the combination - * @return The last move of the combination - */ - [[nodiscard]] [[maybe_unused]] AtomMove getLastMove() const { - return moves.back(); - } - - // implement == operator for AtomMove [[nodiscard]] bool operator==(const MoveComb& other) const { return moves == other.moves; } + /** + * @brief Inequality comparison. + * @param other Other combination. + * @return True if move sequence differs. + */ [[nodiscard]] bool operator!=(const MoveComb& other) const { return !(*this == other); } /** - * @brief Append a single move to the end of the combination. - * @param addMove The move to append + * @brief Append a single move. + * @param addMove Move to append. + * @note Resets cost to +inf for later recomputation. */ void append(AtomMove addMove) { moves.emplace_back(addMove); cost = std::numeric_limits::max(); } /** - * @brief Append all moves of another combination to the end of this one. - * @param addMoveComb The other combination to append + * @brief Concatenate moves from another combination. + * @param addMoveComb Source combination. + * @note Resets cost to +inf for later recomputation. */ void append(const MoveComb& addMoveComb) { moves.insert(moves.end(), addMoveComb.moves.begin(), addMoveComb.moves.end()); cost = std::numeric_limits::max(); } + /** + * @brief Number of moves contained. + * @return Size of move sequence. + */ [[nodiscard]] size_t size() const { return moves.size(); } + /** + * @brief Check emptiness. + * @return True if no moves stored. + */ [[nodiscard]] bool empty() const { return moves.empty(); } }; /** - * @brief Helper class to manage multiple move combinations. + * @brief Container managing a set of unique move combinations. + * @detail Prevents duplicate sequences, propagates operation metadata, and + * offers pruning utilities. */ struct MoveCombs { std::vector moveCombs; @@ -180,7 +336,15 @@ struct MoveCombs { explicit MoveCombs(std::vector combs) : moveCombs(std::move(combs)) {} + /** + * @brief Check if container is empty. + * @return True if no combinations stored. + */ [[nodiscard]] bool empty() const { return moveCombs.empty(); } + /** + * @brief Number of stored combinations. + * @return Count of combinations. + */ [[nodiscard]] size_t size() const { return moveCombs.size(); } // define iterators that iterate over the moveCombs vector @@ -192,30 +356,114 @@ struct MoveCombs { [[nodiscard]] const_iterator end() const { return moveCombs.cend(); } /** - * @brief Add a move combination to the list of move combinations. - * @param moveComb The move combination to add. + * @brief Set associated operation and best positions for all combinations. + * @param op Operation pointer (non-owning). + * @param pos Best coordinate indices for op. + */ + void setOperation(const qc::Operation* op, const CoordIndices& pos) { + for (auto& moveComb : moveCombs) { + moveComb.op = op; + moveComb.bestPos = pos; + } + } + + /** + * @brief Insert a combination, deduplicating existing sequences. + * @details If an equal sequence exists, that existing entry's cost is + * invalidated (set to +inf) instead of inserting a duplicate. + * @param moveComb Combination to add. */ void addMoveComb(const MoveComb& moveComb); /** - * @brief Add all move combinations of another MoveCombs object to the list of - * move combinations. - * @param otherMoveCombs The other MoveCombs object to add. + * @brief Bulk insert all combinations from another container. + * @param otherMoveCombs Source container. */ void addMoveCombs(const MoveCombs& otherMoveCombs); /** - * @brief Remove all move combinations that are longer than the shortest move - * combination. + * @brief Prune combinations longer than the current minimum length. + * @details Computes minimal number of moves and removes any with a greater + * count. */ void removeLongerMoveCombs(); }; /** - * @brief Helper struct to store the position of a multi qubit gate and the - * number of moves needed to execute it. + * @brief Position and move count summary for a multi-qubit gate. + * @param coords Coordinate indices involved. + * @param nMoves Number of atom moves required. */ struct MultiQubitMovePos { CoordIndices coords; size_t nMoves{0}; }; +/** + * @brief Precomputes CZ-based bridge circuits and gate metrics for linear + * chains. + * @details For lengths in [3, maxLength) stores circuits plus aggregate H/CZ + * counts and per-qubit depth maxima to support cost estimation. + */ +class BridgeCircuits { +public: + /** Bridge circuits indexed by chain length (number of qubits). */ + std::vector bridgeCircuits; + /** Total number of H gates per length. */ + std::vector hs; + /** Total number of CZ gates per length (counted via Z after MCX->MCZ). */ + std::vector czs; + /** Maximum number of H gates on any single qubit for a given length. */ + std::vector hDepth; + /** Maximum number of CZ involvements on any single qubit for a length. */ + std::vector czDepth; + + /** + * @brief Construct and precompute bridge data up to given length. + * @param maxLength Exclusive upper bound on chain length table size. + */ + explicit BridgeCircuits(const size_t maxLength) { + bridgeCircuits.resize(maxLength, qc::QuantumComputation()); + hs.resize(maxLength, 0); + czs.resize(maxLength, 0); + hDepth.resize(maxLength, 0); + czDepth.resize(maxLength, 0); + for (size_t i = 3; i < maxLength; ++i) { + computeBridgeCircuit(i); + computeGates(i); + } + } + +protected: + /** + * @brief Compute aggregate gate counts and per-qubit depth metrics. + * @param length Chain length whose circuit has been generated. + */ + void computeGates(size_t length); + /** + * @brief Build base bridge circuit for a chain length. + * @param length Target chain length. + * @details Starts from length 3 pattern and recurses via minimal-load + * expansion. + */ + void computeBridgeCircuit(size_t length); + + /** + * @brief Recursively expand circuit choosing insertion minimizing current + * load. + * @param qcBridge Existing bridge circuit. + * @param length Desired final length. + * @return Expanded circuit of requested length. + */ + static qc::QuantumComputation + recursiveBridgeIncrease(qc::QuantumComputation qcBridge, size_t length); + + /** + * @brief Expand circuit by inserting a qubit between positions qubit and + * qubit+1. + * @param qcBridge Circuit to expand. + * @param qubit Insertion position. + * @return New circuit with inserted qubit. + */ + static qc::QuantumComputation + bridgeExpand(const qc::QuantumComputation& qcBridge, size_t qubit); +}; } // namespace na diff --git a/python/mqt/qmap/hybrid_mapper.pyi b/python/mqt/qmap/hybrid_mapper.pyi index 268bce68b..21d2985a0 100644 --- a/python/mqt/qmap/hybrid_mapper.pyi +++ b/python/mqt/qmap/hybrid_mapper.pyi @@ -8,8 +8,8 @@ """Python bindings for hybrid mapper module.""" +import typing from enum import Enum -from typing import overload from mqt.core.ir import QuantumComputation @@ -19,64 +19,42 @@ class InitialCoordinateMapping(Enum): class InitialCircuitMapping(Enum): identity = ... + graph = ... -class HybridMapperParameters: +class MapperParameters: decay: float + dynamic_mapping_weight: float gate_weight: float - initial_mapping: InitialCoordinateMapping + initial_coord_mapping: InitialCoordinateMapping + limit_shuttling_layer: int + lookahead_depth: int lookahead_weight_moves: float lookahead_weight_swaps: float + max_bridge_distance: int + num_flying_ancillas: int seed: int shuttling_time_weight: float shuttling_weight: float + use_pass_by: bool verbose: bool - @overload def __init__(self) -> None: ... - @overload - def __init__( - self, - lookahead_weight_swaps: float = ..., - lookahead_weight_moves: float = ..., - decay: float = ..., - shuttling_time_weight: float = ..., - gate_weight: float = ..., - shuttling_weight: float = ..., - ) -> None: ... -class HybridNAMapper: - """The hybrid mapper for Neutral Atom Quantum Computers.""" - def __init__(self, arch: NeutralAtomHybridArchitecture, params: HybridMapperParameters = ...) -> None: ... - def get_animation_csv(self) -> str: ... - def get_init_hw_pos(self) -> dict[int, int]: ... - def get_mapped_qc(self) -> str: ... - def get_mapped_qc_aod(self) -> str: ... - def map( - self, circ: QuantumComputation, initial_mapping: InitialCircuitMapping = ..., verbose: bool = ... - ) -> None: ... - def map_qasm_file( - self, filename: str, initial_mapping: InitialCircuitMapping = ..., verbose: bool = ... - ) -> None: ... - def save_animation_csv(self, filename: str) -> None: ... - def save_mapped_qc(self, filename: str) -> None: ... - def save_mapped_qc_aod(self, filename: str) -> None: ... - def schedule( - self, verbose: bool = ..., create_animation_csv: bool = ..., shuttling_speed_factor: float = ... - ) -> dict[str, float]: ... - def set_parameters(self, params: HybridMapperParameters) -> None: ... +class MapperStats: + num_bridges: int + num_f_ancillas: int + num_moves: int + num_pass_by: int + num_swaps: int + def __init__(self) -> None: ... class NeutralAtomHybridArchitecture: - """Class representing the architecture of a Neutral Atom Quantum Computer.""" - name: str - def __init__(self, filename: str) -> None: ... - def compute_swap_distance(self, idx1: int, idx2: int) -> float: ... - def get_animation_csv(self) -> str: ... + def compute_swap_distance(self, idx1: typing.SupportsInt, idx2: typing.SupportsInt) -> int: ... def get_gate_average_fidelity(self, s: str) -> float: ... def get_gate_time(self, s: str) -> float: ... - def get_nearby_coordinates(self, idx: int) -> set[int]: ... + def get_nearby_coordinates(self, idx: typing.SupportsInt) -> set[int]: ... def load_json(self, json_filename: str) -> None: ... - def save_animation_csv(self, filename: str) -> None: ... @property def blocking_factor(self) -> float: ... @property @@ -90,12 +68,54 @@ class NeutralAtomHybridArchitecture: @property def naod_intermediate_levels(self) -> int: ... @property - def naods(self) -> int: ... + def num_aods(self) -> int: ... @property - def ncolumns(self) -> int: ... + def num_columns(self) -> int: ... @property - def npositions(self) -> int: ... + def num_positions(self) -> int: ... @property - def nqubits(self) -> int: ... + def num_qubits(self) -> int: ... @property - def nrows(self) -> int: ... + def num_rows(self) -> int: ... + +class HybridNAMapper: + def __init__(self, arch: NeutralAtomHybridArchitecture, params: MapperParameters) -> None: ... + def get_animation_viz(self) -> str: ... + def get_init_hw_pos(self) -> dict[int, int]: ... + def get_mapped_qc_qasm(self) -> str: ... + def get_mapped_qc_aod_qasm(self) -> str: ... + def get_stats(self) -> dict[str, float]: ... + def map(self, circ: QuantumComputation, initial_mapping: InitialCircuitMapping = ...) -> None: ... + def map_qasm_file(self, filename: str, initial_mapping: InitialCircuitMapping = ...) -> None: ... + def save_animation_files(self, filename: str) -> None: ... + def save_mapped_qc_aod_qasm(self, filename: str) -> None: ... + def schedule( + self, + verbose: bool = ..., + create_animation_csv: bool = ..., + shuttling_speed_factor: typing.SupportsFloat = ..., + ) -> dict[str, float]: ... + def set_parameters(self, params: MapperParameters) -> None: ... + +# noinspection DuplicatedCode +class HybridSynthesisMapper: + def __init__(self, arch: NeutralAtomHybridArchitecture, params: MapperParameters = ...) -> None: ... + def append_with_mapping(self, qc: QuantumComputation) -> None: ... + def append_without_mapping(self, qc: QuantumComputation) -> None: ... + def complete_remap(self, initial_mapping: InitialCircuitMapping = ...) -> None: ... + def convert_to_aod(self) -> None: ... + def evaluate_synthesis_steps( + self, synthesis_steps: list[QuantumComputation], also_map: bool = ... + ) -> list[float]: ... + def get_circuit_adjacency_matrix(self) -> list[list[int]]: ... + def get_mapped_qc_qasm(self) -> str: ... + def get_mapped_qc_aod_qasm(self) -> str: ... + def get_synthesized_qc_qasm(self) -> str: ... + def init_mapping(self, n_qubits: typing.SupportsInt) -> None: ... + def save_mapped_qc_qasm(self, filename: str) -> None: ... + def save_mapped_qc_aod_qasm(self, filename: str) -> None: ... + def save_synthesized_qc_qasm(self, filename: str) -> None: ... + def schedule( + self, verbose: bool = ..., create_animation_csv: bool = ..., shuttling_speed_factor: typing.SupportsFloat = ... + ) -> dict[str, float]: ... + def set_parameters(self, params: MapperParameters) -> None: ... diff --git a/src/hybridmap/CMakeLists.txt b/src/hybridmap/CMakeLists.txt index bb37cf827..bd62e5b7f 100644 --- a/src/hybridmap/CMakeLists.txt +++ b/src/hybridmap/CMakeLists.txt @@ -28,7 +28,7 @@ if(NOT TARGET ${MQT_QMAP_HYBRIDMAP_TARGET_NAME}) # link to the MQT::Core libraries target_link_libraries( ${MQT_QMAP_HYBRIDMAP_TARGET_NAME} - PUBLIC MQT::CoreIR MQT::CoreNA nlohmann_json::nlohmann_json + PUBLIC MQT::CoreIR MQT::CoreNA nlohmann_json::nlohmann_json spdlog::spdlog PRIVATE MQT::ProjectWarnings MQT::ProjectOptions MQT::CoreCircuitOptimizer) # add MQT alias diff --git a/src/hybridmap/HardwareQubits.cpp b/src/hybridmap/HardwareQubits.cpp index 23bc277d8..a1fde115b 100644 --- a/src/hybridmap/HardwareQubits.cpp +++ b/src/hybridmap/HardwareQubits.cpp @@ -16,6 +16,7 @@ #include "ir/Definitions.hpp" #include +#include #include #include #include @@ -27,8 +28,8 @@ namespace na { void HardwareQubits::initTrivialSwapDistances() { - swapDistances = qc::SymmetricMatrix(arch->getNqubits()); - for (uint32_t i = 0; i < arch->getNqubits(); ++i) { + swapDistances = qc::SymmetricMatrix(nQubits); + for (uint32_t i = 0; i < nQubits; ++i) { for (uint32_t j = 0; j < i; ++j) { swapDistances(i, j) = arch->getSwapDistance(hwToCoordIdx.at(i), hwToCoordIdx.at(j)); @@ -37,15 +38,15 @@ void HardwareQubits::initTrivialSwapDistances() { } void HardwareQubits::initNearbyQubits() { - for (uint32_t i = 0; i < arch->getNqubits(); ++i) { + for (uint32_t i = 0; i < nQubits; ++i) { computeNearbyQubits(i); } } -void HardwareQubits::computeSwapDistance(HwQubit q1, HwQubit q2) { +void HardwareQubits::computeSwapDistance(HwQubit q1, const HwQubit q2) { std::queue q; - std::vector visited(swapDistances.size(), false); - std::vector parent(swapDistances.size(), q2); + std::vector visited(swapDistances.size(), false); + std::vector parent(swapDistances.size(), q2); q.push(q1); visited[q1] = true; @@ -87,35 +88,89 @@ void HardwareQubits::computeSwapDistance(HwQubit q1, HwQubit q2) { } } +std::vector +HardwareQubits::computeAllShortestPaths(const HwQubit q1, + const HwQubit q2) const { + std::vector allPaths; + std::queue pathsQueue; + auto shortestPathLength = std::numeric_limits::max(); + + pathsQueue.push(HwQubitsVector{q1}); + + while (!pathsQueue.empty()) { + auto currentPath = pathsQueue.front(); + pathsQueue.pop(); + + if (currentPath.size() > shortestPathLength) { + continue; + } + + HwQubit const currentQubit = currentPath.back(); + + if (currentQubit == q2) { + if (shortestPathLength == std::numeric_limits::max()) { + shortestPathLength = currentPath.size(); + } + if (currentPath.size() == shortestPathLength) { + allPaths.push_back(currentPath); + } + continue; + } + + for (const auto& neighbor : getNearbyQubits(currentQubit)) { + if (std::ranges::find(currentPath, neighbor) == currentPath.end()) { + auto newPath = currentPath; + newPath.push_back(neighbor); + pathsQueue.push(newPath); + } + } + } + + return allPaths; +} void HardwareQubits::resetSwapDistances() { // TODO Improve to only reset the swap distances necessary (use a breadth // first search) - swapDistances = qc::SymmetricMatrix(arch->getNqubits(), -1); + swapDistances = qc::SymmetricMatrix(nQubits, -1); } -void HardwareQubits::move(HwQubit hwQubit, CoordIndex newCoord) { +void HardwareQubits::move(HwQubit hwQubit, const CoordIndex newCoord) { if (newCoord >= arch->getNpositions()) { throw std::runtime_error("Invalid coordinate"); } // check if new coordinate is already occupied - for (const auto& [qubit, coord] : hwToCoordIdx) { + for (const auto& coord : hwToCoordIdx | std::views::values) { if (coord == newCoord) { throw std::runtime_error("Coordinate already occupied"); } } + const auto oldCoord = hwToCoordIdx.at(hwQubit); + if (const auto it = std::ranges::find(occupiedCoordinates, oldCoord); + it != occupiedCoordinates.end()) { + occupiedCoordinates.erase(it); + } + occupiedCoordinates.emplace_back(newCoord); + freeCoordinates.emplace_back(oldCoord); + if (const auto it2 = std::ranges::find(freeCoordinates, newCoord); + it2 != freeCoordinates.end()) { + freeCoordinates.erase(it2); + } + // remove qubit from old nearby qubits - auto prevNearbyQubits = nearbyQubits.at(hwQubit); + const auto prevNearbyQubits = nearbyQubits.at(hwQubit); for (const auto& qubit : prevNearbyQubits) { - nearbyQubits.at(qubit).erase(std::find( - nearbyQubits.at(qubit).begin(), nearbyQubits.at(qubit).end(), hwQubit)); + auto& neigh = nearbyQubits.at(qubit); + if (auto it3 = std::ranges::find(neigh, hwQubit); it3 != neigh.end()) { + neigh.erase(it3); + } } // move qubit and compute new nearby qubits hwToCoordIdx.at(hwQubit) = newCoord; computeNearbyQubits(hwQubit); // add qubit to new nearby qubits - auto newNearbyQubits = nearbyQubits.at(hwQubit); + const auto newNearbyQubits = nearbyQubits.at(hwQubit); for (const auto& qubit : newNearbyQubits) { nearbyQubits.at(qubit).emplace(hwQubit); } @@ -133,11 +188,11 @@ std::vector HardwareQubits::getNearbySwaps(HwQubit q) const { return swaps; } -void HardwareQubits::computeNearbyQubits(HwQubit q) { +void HardwareQubits::computeNearbyQubits(const HwQubit qubit) { std::set newNearbyQubits; - auto coordQ = hwToCoordIdx.at(q); + const auto coordQ = hwToCoordIdx.at(qubit); for (const auto& coord : hwToCoordIdx) { - if (coord.first == q) { + if (coord.first == qubit) { continue; } if (arch->getEuclideanDistance(coordQ, coord.second) <= @@ -145,15 +200,15 @@ void HardwareQubits::computeNearbyQubits(HwQubit q) { newNearbyQubits.emplace(coord.first); } } - nearbyQubits.insert_or_assign(q, newNearbyQubits); + nearbyQubits.insert_or_assign(qubit, newNearbyQubits); } qc::fp HardwareQubits::getAllToAllSwapDistance(std::set& qubits) { // two qubit gates if (qubits.size() == 2) { auto it = qubits.begin(); - auto q1 = *it; - auto q2 = *(++it); + const auto q1 = *it; + const auto q2 = *++it; return getSwapDistance(q1, q2); } // for n > 2 all qubits need to be within the interaction radius of each other @@ -167,10 +222,10 @@ qc::fp HardwareQubits::getAllToAllSwapDistance(std::set& qubits) { } std::set -HardwareQubits::getBlockedQubits(const std::set& qubits) { +HardwareQubits::getBlockedQubits(const std::set& qubits) const { std::set blockedQubits; for (const auto& qubit : qubits) { - for (uint32_t i = 0; i < arch->getNqubits(); ++i) { + for (uint32_t i = 0; i < hwToCoordIdx.maxKey(); ++i) { if (i == qubit) { continue; } @@ -187,59 +242,79 @@ HardwareQubits::getBlockedQubits(const std::set& qubits) { } std::set -HardwareQubits::getNearbyFreeCoordinatesByCoord(CoordIndex idx) { +HardwareQubits::getNearbyFreeCoordinatesByCoord(const CoordIndex idx) const { std::set nearbyFreeCoordinates; - for (auto const& coordIndex : this->arch->getNearbyCoordinates(idx)) { - if (!this->isMapped(coordIndex)) { + for (auto const& coordIndex : arch->getNearbyCoordinates(idx)) { + if (!isMapped(coordIndex)) { nearbyFreeCoordinates.emplace(coordIndex); } } return nearbyFreeCoordinates; } -std::set -HardwareQubits::getNearbyOccupiedCoordinatesByCoord(CoordIndex idx) const { - auto nearbyHwQubits = this->getNearbyQubits(this->getHwQubit(idx)); - return this->getCoordIndices(nearbyHwQubits); +std::set HardwareQubits::getNearbyOccupiedCoordinatesByCoord( + const CoordIndex idx) const { + const auto nearbyHwQubits = getNearbyQubits(getHwQubit(idx)); + return getCoordIndices(nearbyHwQubits); } std::vector -HardwareQubits::findClosestFreeCoord(CoordIndex coord, Direction direction, - const CoordIndices& excludeCoord) { - // return the closest free coord in general - // and the closest free coord in the given direction - std::vector closestFreeCoords; - std::queue queue; - queue.push(coord); - std::set visited; - visited.emplace(coord); - bool foundClosest = false; - while (!queue.empty()) { - auto currentCoord = queue.front(); - queue.pop(); - auto nearbyCoords = this->arch->getNN(currentCoord); - for (const auto& nearbyCoord : nearbyCoords) { - if (std::ranges::find(std::ranges::reverse_view(visited), nearbyCoord) == - visited.rend()) { - visited.emplace(nearbyCoord); - if (!this->isMapped(nearbyCoord) && - std::ranges::find(excludeCoord, nearbyCoord) == - excludeCoord.end()) { - if (!foundClosest) { - closestFreeCoords.emplace_back(nearbyCoord); - } - foundClosest = true; - if (direction == arch->getVector(coord, nearbyCoord).direction) { - closestFreeCoords.emplace_back(nearbyCoord); - return closestFreeCoords; - } - } else { - queue.push(nearbyCoord); - } +HardwareQubits::findClosestFreeCoord(const CoordIndex coord, + const Direction direction, + const CoordIndices& excludedCoords) const { + std::vector freeCoordsInDirection; + for (const auto& freeCoord : freeCoordinates) { + if (std::ranges::find(excludedCoords, freeCoord) != excludedCoords.end()) { + continue; + } + if (direction == arch->getVector(coord, freeCoord).direction) { + freeCoordsInDirection.emplace_back(freeCoord); + } + } + if (freeCoordsInDirection.empty()) { + // return all free coords except excluded + auto allFreeCoords = freeCoordinates; + for (const auto& excludedCoord : excludedCoords) { + if (const auto pos = std::ranges::find(allFreeCoords, excludedCoord); + pos != allFreeCoords.end()) { + allFreeCoords.erase(pos); } } + return allFreeCoords; + } + auto minDistance = std::numeric_limits::max(); + CoordIndex minCoord = freeCoordsInDirection.front(); + for (const auto& freeCoord : freeCoordsInDirection) { + if (const auto distance = arch->getEuclideanDistance(coord, freeCoord); + distance < minDistance) { + minDistance = distance; + minCoord = freeCoord; + } + } + return {minCoord}; +} + +HwQubit HardwareQubits::getClosestQubit(const CoordIndex coord, + const HwQubits& ignored) const { + HwQubit closestQubit = 0; + bool noneFound = true; + auto minDistance = std::numeric_limits::max(); + for (auto const& [qubit, idx] : hwToCoordIdx) { + if (ignored.contains(qubit)) { + continue; + } + if (const auto distance = arch->getEuclideanDistance(coord, idx); + distance < minDistance) { + minDistance = distance; + closestQubit = qubit; + noneFound = false; + } + } + if (noneFound) { + throw std::runtime_error( + "No available qubit found when searching for closest qubit."); } - return closestFreeCoords; + return closestQubit; } } // namespace na diff --git a/src/hybridmap/HybridAnimation.cpp b/src/hybridmap/HybridAnimation.cpp index d90910918..8e8877ee0 100644 --- a/src/hybridmap/HybridAnimation.cpp +++ b/src/hybridmap/HybridAnimation.cpp @@ -16,212 +16,177 @@ #include "ir/operations/AodOperation.hpp" #include "ir/operations/OpType.hpp" +#include +#include #include -#include #include +#include #include #include #include #include namespace na { -AnimationAtoms::AnimationAtoms(const std::map& initHwPos, - const NeutralAtomArchitecture& arch) { - auto nCols = arch.getNcolumns(); - +void AnimationAtoms::initPositions( + const std::map& initHwPos, + const std::map& initFaPos) { + const auto nCols = arch->getNcolumns(); for (const auto& [id, coord] : initHwPos) { coordIdxToId[coord] = id; - auto column = coord % nCols; - auto row = coord / nCols; - idToCoord[id] = {column * arch.getInterQubitDistance(), - row * arch.getInterQubitDistance()}; + const auto column = coord % nCols; + const auto row = coord / nCols; + idToCoord[id] = {column * arch->getInterQubitDistance(), + row * arch->getInterQubitDistance()}; } -} -std::string AnimationAtoms::getInitString() { - std::string initString; - initString += - "time;id;x;y;size;fill;color;axes;axesId;margin;marginId;marginSize\n"; - for (const auto& [id, coord] : idToCoord) { - initString += "0.000;" + std::to_string(id) + ";" + - std::to_string(coord.first) + ";" + - std::to_string(coord.second) + ";1;0;0;0;0;0;0;0\n"; + auto flyingAncillaIdxPlusOne = 0; + const auto hwCount = static_cast(initHwPos.size()); + for (const auto& [id, coord] : initFaPos) { + flyingAncillaIdxPlusOne++; + coordIdxToId[(coord + static_cast(2 * arch->getNpositions()))] = + id + hwCount; + const auto column = coord % nCols; + const auto row = coord / nCols; + const auto offset = + arch->getInterQubitDistance() / arch->getNAodIntermediateLevels(); + idToCoord[(id + hwCount)] = {(column * arch->getInterQubitDistance()) + + flyingAncillaIdxPlusOne * offset, + (row * arch->getInterQubitDistance()) + + flyingAncillaIdxPlusOne * offset}; } - return initString; } -std::string AnimationAtoms::getEndString(qc::fp endTime) { +std::string AnimationAtoms::placeInitAtoms() const { std::string initString; - for (const auto& [id, coord] : idToCoord) { - initString += std::to_string(endTime) + ";" + std::to_string(id) + ";" + - std::to_string(coord.first) + ";" + - std::to_string(coord.second) + ";1;0;0;0;0;0;0;0\n"; + for (const auto& [id, coords] : idToCoord) { + initString += "atom (" + std::to_string(coords.first) + ", " + + std::to_string(coords.second) + ") atom" + + std::to_string(id) + "\n"; } return initString; } +std::string AnimationAtoms::opToNaViz(const std::unique_ptr& op, + qc::fp startTime) { + std::string opString; -AnimationAtoms::axesId AnimationAtoms::addAxis(HwQubit id) { - if (!axesIds.contains(id)) { - axesIdCounter++; - axesIds[id] = axesIdCounter; - } else { - throw std::invalid_argument( - "Tried to add axis but axis already exists for qubit " + - std::to_string(id)); - } - return axesIds[id]; -} -AnimationAtoms::marginId AnimationAtoms::addMargin(HwQubit id) { - if (!marginIds.contains(id)) { - marginIdCounter++; - marginIds[id] = marginIdCounter; - } else { - throw std::invalid_argument( - "Tried to add margin but margin already exists for qubit " + - std::to_string(id)); - } - return marginIds[id]; -} - -std::string -AnimationAtoms::createCsvOp(const std::unique_ptr& op, - qc::fp startTime, qc::fp endTime, - const NeutralAtomArchitecture& arch) { - std::string csvLine; - - for (const auto& coordIdx : op->getUsedQubits()) { - // if coordIdx unmapped -> continue except it is an AodDeactivate - if (qc::OpType::AodDeactivate != op->getType() && - !coordIdxToId.contains(coordIdx)) { - continue; + if (op->getType() == qc::OpType::AodActivate) { + opString += "@" + std::to_string(startTime) + " load [\n"; + for (const auto& coordIdx : op->getTargets()) { + const auto id = coordIdxToId.at(coordIdx); + opString += "\t atom" + std::to_string(id) + "\n"; } - if (op->getType() == qc::OpType::AodDeactivate) { - // check if there is a qubit at coordIdx - // if yes -> update coordIdxToId with new coordIdx - // if not -> throw exception - for (const auto& idAndCoord : idToCoord) { - auto id = idAndCoord.first; - auto coord = idAndCoord.second; - auto col = coordIdx % arch.getNcolumns(); - auto row = coordIdx / arch.getNcolumns(); - if (std::abs(coord.first - col * arch.getInterQubitDistance()) < - 0.0001 && - std::abs(coord.second - row * arch.getInterQubitDistance()) < - 0.0001) { - // remove old coordIdx with same id - for (const auto& [oldCoordIdx, oldId] : coordIdxToId) { - if (oldId == id) { - coordIdxToId.erase(oldCoordIdx); - break; - } - } - // add new coordIdx with id - coordIdxToId[coordIdx] = id; - break; - } - } + opString += "]\n"; + } else if (op->getType() == qc::OpType::AodDeactivate) { + opString += "@" + std::to_string(startTime) + " store [\n"; + for (const auto& coordIdx : op->getTargets()) { + const auto id = coordIdxToId.at(coordIdx); + opString += "\t atom" + std::to_string(id) + "\n"; } - if (!coordIdxToId.contains(coordIdx) || - !idToCoord.contains(coordIdxToId.at(coordIdx))) { - throw std::invalid_argument( - "Tried to create csv line for qubit at coordIdx " + - std::to_string(coordIdx) + " but there is no qubit at this coordIdx"); + opString += "]\n"; + } else if (op->getType() == qc::OpType::AodMove) { + // update atom coordinates + const auto* aodOp = dynamic_cast(op.get()); + assert(aodOp != nullptr && + "OpType::AodMove must be backed by AodOperation"); + const auto startsX = aodOp->getStarts(Dimension::X); + const auto endsX = aodOp->getEnds(Dimension::X); + const auto startsY = aodOp->getStarts(Dimension::Y); + const auto endsY = aodOp->getEnds(Dimension::Y); + assert(startsX.size() == endsX.size()); + assert(startsY.size() == endsY.size()); + const auto& coordIndices = op->getTargets(); // renamed + // The list of targets for an AodMove operation must contain pairs of + // (origin, destination) coordinate indices. + if (coordIndices.size() % 2 != 0) { + throw std::logic_error( + "AodMove targets must be pairs of origin and target indices."); } - auto id = coordIdxToId.at(coordIdx); - auto coord = idToCoord.at(id); - if (op->getType() == qc::OpType::AodActivate) { - addAxis(id); - if (!axesIds.contains(id)) { - throw std::invalid_argument( - "Tried to activate qubit at coordIdx " + std::to_string(coordIdx) + - " but there is no axis for qubit " + std::to_string(id)); - } - csvLine += createCsvLine(startTime, id, coord.first, coord.second, 1, - colorSlm, true, axesIds.at(id)); - csvLine += createCsvLine(endTime, id, coord.first, coord.second, 1, - colorAod, true, axesIds.at(id)); - } else if (op->getType() == qc::OpType::AodDeactivate) { - if (!axesIds.contains(id)) { - throw std::invalid_argument("Tried to deactivate qubit at coordIdx " + - std::to_string(coordIdx) + - " but there is no axis for qubit " + - std::to_string(id)); - } - csvLine += createCsvLine(startTime, id, coord.first, coord.second, 1, - colorAod, true, axesIds.at(id)); - csvLine += createCsvLine(endTime, id, coord.first, coord.second, 1, - colorSlm, true, axesIds.at(id)); - removeAxis(id); - } else if (op->getType() == qc::OpType::AodMove) { - if (!axesIds.contains(id)) { - throw std::invalid_argument( - "Tried to move qubit at coordIdx " + std::to_string(coordIdx) + - " but there is no axis for qubit " + std::to_string(id)); - } - csvLine += createCsvLine(startTime, id, coord.first, coord.second, 1, - colorAod, true, axesIds.at(id)); + // Tolerance for floating point comparisons when matching start coordinates. - // update atom coordinates - auto startsX = - dynamic_cast(op.get())->getStarts(Dimension::X); - auto endsX = dynamic_cast(op.get())->getEnds(Dimension::X); - auto startsY = - dynamic_cast(op.get())->getStarts(Dimension::Y); - auto endsY = dynamic_cast(op.get())->getEnds(Dimension::Y); - for (size_t i = 0; i < startsX.size(); i++) { - if (std::abs(startsX[i] - coord.first) < 0.0001) { - coord.first = endsX[i]; + // use that coord indices are pairs of origin and target indices + for (size_t i = 0; i < coordIndices.size(); i++) { + if (i % 2 == 0) { + constexpr qc::fp fpTolerance = 0.0001; + const auto coordIdx = coordIndices[i]; + if (!coordIdxToId.contains(coordIdx)) { + throw std::logic_error("AodMove origin index " + + std::to_string(coordIdx) + + " not found in coordIdxToId map."); } - } - for (size_t i = 0; i < startsY.size(); i++) { - if (std::abs(startsY[i] - coord.second) < 0.0001) { - coord.second = endsY[i]; + const auto id = coordIdxToId.at(coordIdx); + if (!idToCoord.contains(id)) { + throw std::logic_error("Atom ID " + std::to_string(id) + + " not found in idToCoord map."); + } + bool foundX = false; + auto newX = std::numeric_limits::max(); + bool foundY = false; + auto newY = std::numeric_limits::max(); + for (size_t j = 0; j < startsX.size(); j++) { + if (std::abs(startsX[j] - idToCoord.at(id).first) < fpTolerance) { + newX = endsX[j]; + foundX = true; + break; + } + } + if (!foundX) { + // X coord is the same as before if no matching start is found. + newX = idToCoord.at(id).first; } - } - // save new coordinates - idToCoord[id] = coord; - csvLine += createCsvLine(endTime, id, coord.first, coord.second, 1, - colorAod, true, axesIds.at(id)); - } else if (op->getUsedQubits().size() > 1) { // multi qubit gates - addMargin(id); - csvLine += createCsvLine(startTime, id, coord.first, coord.second, 1, - colorSlm, false, 0, false, marginIds.at(id)); - auto midTime = (startTime + endTime) / 2; - csvLine += - createCsvLine(midTime, id, coord.first, coord.second, 1, colorCz, - false, 0, true, marginIds.at(id), - arch.getBlockingFactor() * arch.getInteractionRadius() * - arch.getInterQubitDistance()); - csvLine += createCsvLine(endTime, id, coord.first, coord.second, 1, - colorSlm, false, 0, false, marginIds.at(id)); - removeMargin(id); - } else { // single qubit gates - csvLine += - createCsvLine(startTime, id, coord.first, coord.second, 1, colorSlm); - auto midTime = (startTime + endTime) / 2; - csvLine += - createCsvLine(midTime, id, coord.first, coord.second, 1, colorLocal); - csvLine += - createCsvLine(endTime, id, coord.first, coord.second, 1, colorSlm); + for (size_t j = 0; j < startsY.size(); j++) { + if (std::abs(startsY[j] - idToCoord.at(id).second) < fpTolerance) { + newY = endsY[j]; + foundY = true; + break; + } + } + if (!foundY) { + // Y coord is the same as before if no matching start is found. + newY = idToCoord.at(id).second; + } + opString += "@" + std::to_string(startTime) + " move (" + + std::to_string(newX) + ", " + std::to_string(newY) + + ") atom" + std::to_string(id) + "\n"; + auto& coords = idToCoord.at(id); + coords.first = newX; + coords.second = newY; + } else { + // this is the target index -> update coordIdxToId + const auto coordIdx = coordIndices[i]; + const auto prevCoordIdx = coordIndices[i - 1]; + if (!coordIdxToId.contains(prevCoordIdx)) { + throw std::logic_error( + "AodMove origin index " + std::to_string(prevCoordIdx) + + " not found in coordIdxToId map during update."); + } + const auto id = coordIdxToId.at(prevCoordIdx); + coordIdxToId.erase(prevCoordIdx); + coordIdxToId[coordIdx] = id; + } } + // must be a gate + // For visualization: + // - All multi-qubit gates → cz + // - All single-qubit gates → rz + } else if (op->getNqubits() > 1) { + opString += "@" + std::to_string(startTime) + " cz {"; + for (const auto& coordIdx : op->getUsedQubits()) { + const auto id = coordIdxToId.at(coordIdx); + opString += " atom" + std::to_string(id) + ","; + } + opString.pop_back(); + opString += "}\n"; + } else { + // single qubit gate + const auto coordIdx = op->getTargets().front(); + const auto id = coordIdxToId.at(coordIdx); + opString += "@" + std::to_string(startTime) + " rz 1" + " atom" + + std::to_string(id) + "\n"; } - return csvLine; -} -std::string AnimationAtoms::createCsvLine( - qc::fp startTime, HwQubit id, qc::fp x, qc::fp y, uint32_t size, - uint32_t color, bool axes, AnimationAtoms::axesId axId, bool margin, - AnimationAtoms::marginId marginId, qc::fp marginSize) { - std::string csvLine; - csvLine += - std::to_string(startTime) + ";" + std::to_string(id) + ";" + - std::to_string(x) + ";" + std::to_string(y) + ";" + std::to_string(size) + - ";" + std::to_string(color) + ";" + std::to_string(color) + ";" + - std::to_string(static_cast(axes)) + ";" + std::to_string(axId) + - ";" + std::to_string(static_cast(margin)) + ";" + - std::to_string(marginId) + ";" + std::to_string(marginSize) + "\n"; - return csvLine; + + return opString; } } // namespace na diff --git a/src/hybridmap/HybridNeutralAtomMapper.cpp b/src/hybridmap/HybridNeutralAtomMapper.cpp index 71a58c16e..149e81dda 100644 --- a/src/hybridmap/HybridNeutralAtomMapper.cpp +++ b/src/hybridmap/HybridNeutralAtomMapper.cpp @@ -11,25 +11,30 @@ #include "hybridmap/HybridNeutralAtomMapper.hpp" #include "circuit_optimizer/CircuitOptimizer.hpp" +#include "hybridmap/Mapping.hpp" #include "hybridmap/MoveToAodConverter.hpp" #include "hybridmap/NeutralAtomDefinitions.hpp" #include "hybridmap/NeutralAtomLayer.hpp" #include "hybridmap/NeutralAtomUtils.hpp" #include "ir/Definitions.hpp" #include "ir/QuantumComputation.hpp" +#include "ir/operations/Control.hpp" #include "ir/operations/OpType.hpp" #include "ir/operations/Operation.hpp" +#include "ir/operations/StandardOperation.hpp" #include #include #include #include #include -#include #include #include +#include #include +#include #include +#include #include #include #include @@ -37,136 +42,205 @@ #include namespace na { -qc::QuantumComputation NeutralAtomMapper::map(qc::QuantumComputation& qc, - InitialMapping initialMapping) { - mappedQc = qc::QuantumComputation(arch.getNpositions()); - nMoves = 0; - nSwaps = 0; +void NeutralAtomMapper::mapAppend(qc::QuantumComputation& qc, + const Mapping& initialMapping) { + // remove barriers and measurements + qc::CircuitOptimizer::removeFinalMeasurements(qc); + // check if multi-qubit gates are present + multiQubitGates = false; + for (const auto& op : qc) { + if (op->getUsedQubits().size() > 2) { + // deactivate static mapping + spdlog::warn( + "The circuit contains multi-qubit gates (more than 2 qubits). " + "Bridge gates will NOT be used for mapping."); + + multiQubitGates = true; + break; + } + } + // only add flying ancillas if not already present + if (mappedQc.getNancillae() == 0) { + // ancilla register has indices [npositions, 2*npositions-1] + mappedQc.addAncillaryRegister(this->arch->getNpositions()); + // flying ancilla register has indices [2*npositions, 3*npositions-1] + mappedQc.addAncillaryRegister(this->arch->getNpositions(), "fa"); + } + qc::CircuitOptimizer::replaceMCXWithMCZ(qc); qc::CircuitOptimizer::singleQubitGateFusion(qc); qc::CircuitOptimizer::flattenOperations(qc); qc::CircuitOptimizer::removeFinalMeasurements(qc); - auto dag = qc::CircuitOptimizer::constructDAG(qc); + const auto dag = qc::CircuitOptimizer::constructDAG(qc); + + mapping = initialMapping; - // init mapping - this->mapping = Mapping(qc.getNqubits(), initialMapping); + if (this->parameters.verbose) { + spdlog::info("* Init Coord Mapping w/ [row:{} X col:{}] hardware", + arch->getNrows(), arch->getNcolumns()); + for (uint32_t q = 0; q < qc.getNqubits(); q++) { + const auto hwQubit = this->mapping.getHwQubit(q); + spdlog::info("q {:3} -> h {:3} -> c {:3}", q, hwQubit, + hardwareQubits.getCoordIndex(hwQubit)); + } + } // init layers - NeutralAtomLayer frontLayer(dag); - frontLayer.initLayerOffset(); - mapAllPossibleGates(frontLayer); - NeutralAtomLayer lookaheadLayer(dag); - lookaheadLayer.initLayerOffset(frontLayer.getIteratorOffset()); + NeutralAtomLayer lookaheadLayer(dag, false, this->parameters.lookaheadDepth); + lookaheadLayer.initAllQubits(); + NeutralAtomLayer frontLayer(dag, true, this->parameters.lookaheadDepth); + frontLayer.initAllQubits(); + lookaheadLayer.removeGatesAndUpdate(frontLayer.getGates()); + mapAllPossibleGates(frontLayer, lookaheadLayer); // Checks - if (dag.size() > arch.getNqubits()) { + if (dag.size() > arch->getNqubits()) { throw std::runtime_error("More qubits in circuit than in architecture"); } - // precompute exponential decay weights - this->decayWeights.reserve(this->arch.getNcolumns()); - for (uint32_t i = this->arch.getNcolumns(); i > 0; --i) { - this->decayWeights.emplace_back(std::exp(-this->parameters.decay * i)); - } - - auto i = 0; + // Mapping Loop + size_t i = 0; while (!frontLayer.getGates().empty()) { // assign gates to layers reassignGatesToLayers(frontLayer.getGates(), lookaheadLayer.getGates()); - - // save last swap to prevent immediate swap back - Swap lastSwap = {0, 0}; - - // first do all gate based mapping gates - while (!this->frontLayerGate.empty()) { - GateList gatesToExecute; - while (gatesToExecute.empty()) { - ++i; - if (this->parameters.verbose) { - std::cout << "iteration " << i << '\n'; - } - auto bestSwap = findBestSwap(lastSwap); - lastSwap = bestSwap; - updateMappingSwap(bestSwap); - gatesToExecute = getExecutableGates(frontLayer.getGates()); - } - mapAllPossibleGates(frontLayer); - lookaheadLayer.initLayerOffset(frontLayer.getIteratorOffset()); - reassignGatesToLayers(frontLayer.getGates(), lookaheadLayer.getGates()); - if (this->parameters.verbose) { - printLayers(); - } - } - // then do all shuttling based mapping gates - while (!this->frontLayerShuttling.empty()) { - GateList gatesToExecute; - while (gatesToExecute.empty()) { - ++i; - if (this->parameters.verbose) { - std::cout << "iteration " << i << '\n'; - } - auto bestMove = findBestAtomMove(); - updateMappingMove(bestMove); - gatesToExecute = getExecutableGates(frontLayer.getGates()); - } - mapAllPossibleGates(frontLayer); - lookaheadLayer.initLayerOffset(frontLayer.getIteratorOffset()); - reassignGatesToLayers(frontLayer.getGates(), lookaheadLayer.getGates()); - if (this->parameters.verbose) { - printLayers(); - } + if (this->parameters.verbose) { + spdlog::info("Iteration {}", i); + printLayers(); } + + i = gateBasedMapping(frontLayer, lookaheadLayer, i); + i = shuttlingBasedMapping(frontLayer, lookaheadLayer, i); } + if (this->parameters.verbose) { - std::cout << "nSwaps: " << nSwaps << '\n'; - std::cout << "nMoves: " << nMoves << '\n'; + spdlog::info("nSwaps: {}", stats.nSwaps); + spdlog::info("nBridges: {}", stats.nBridges); + spdlog::info("nFAncillas: {}", stats.nFAncillas); + spdlog::info("nMoves: {}", stats.nMoves); + spdlog::info("nPassBy: {}", stats.nPassBy); } - return mappedQc; } -void NeutralAtomMapper::mapAllPossibleGates(NeutralAtomLayer& layer) { - // map single qubit gates - for (const auto* opPointer : layer.getMappedSingleQubitGates()) { - mapGate(opPointer); - } - layer.removeGatesAndUpdate({}); - // check and map multi qubit gates - auto executableGates = getExecutableGates(layer.getGates()); +void NeutralAtomMapper::mapAllPossibleGates(NeutralAtomLayer& frontLayer, + NeutralAtomLayer& lookaheadLayer) { + auto executableGates = getExecutableGates(frontLayer.getGates()); while (!executableGates.empty()) { - for (const auto* opPointer : layer.getMappedSingleQubitGates()) { - mapGate(opPointer); - } for (const auto* opPointer : executableGates) { mapGate(opPointer); } - layer.removeGatesAndUpdate(executableGates); - executableGates = getExecutableGates(layer.getGates()); + frontLayer.removeGatesAndUpdate(executableGates); + lookaheadLayer.removeGatesAndUpdate(frontLayer.getNewGates()); + executableGates = getExecutableGates(frontLayer.getGates()); } } -qc::QuantumComputation -NeutralAtomMapper::convertToAod(qc::QuantumComputation& qc) { +void NeutralAtomMapper::decomposeBridgeGates(qc::QuantumComputation& qc) const { + auto it = qc.begin(); + while (it != qc.end()) { + if ((*it)->isStandardOperation() && (*it)->getType() == qc::Bridge) { + const auto targets = (*it)->getTargets(); + it = qc.erase(it); + size_t nInserted = 0; + for (const auto& bridgeOp : + this->arch->getBridgeCircuit(targets.size())) { + const auto bridgeQubits = bridgeOp->getUsedQubits(); + if (bridgeOp->getType() == qc::OpType::H) { + it = qc.insert(it, std::make_unique( + targets[*bridgeQubits.begin()], qc::H)); + } else { + it = qc.insert(it, std::make_unique( + qc::Control{targets[*bridgeQubits.begin()]}, + targets[*bridgeQubits.rbegin()], qc::Z)); + } + ++nInserted; + } + // Advance past all inserted operations + for (size_t i = 0; i < nInserted && it != qc.end(); ++i) { + ++it; + } + } else { + ++it; + } + } +} + +qc::QuantumComputation NeutralAtomMapper::convertToAod() { // decompose SWAP gates - qc::CircuitOptimizer::decomposeSWAP(qc, false); - qc::CircuitOptimizer::replaceMCXWithMCZ(qc); - qc::CircuitOptimizer::singleQubitGateFusion(qc); - qc::CircuitOptimizer::flattenOperations(qc); + qc::CircuitOptimizer::decomposeSWAP(mappedQc, false); + // decompose bridge gates + decomposeBridgeGates(mappedQc); + qc::CircuitOptimizer::replaceMCXWithMCZ(mappedQc); + qc::CircuitOptimizer::singleQubitGateFusion(mappedQc); + qc::CircuitOptimizer::flattenOperations(mappedQc); // decompose AOD moves - MoveToAodConverter aodScheduler(arch); - mappedQcAOD = aodScheduler.schedule(qc); + MoveToAodConverter aodScheduler(*arch, hardwareQubits, flyingAncillas); + mappedQcAOD = aodScheduler.schedule(mappedQc); if (this->parameters.verbose) { - std::cout << "nMoveGroups: " << aodScheduler.getNMoveGroups() << '\n'; + spdlog::info("nMoveGroups: {}", aodScheduler.getNMoveGroups()); } return mappedQcAOD; } +void NeutralAtomMapper::applyPassBy(NeutralAtomLayer& frontLayer, + const PassByComb& pbComb) { + const auto opTargets = pbComb.op->getTargets(); + const auto targetHwQubits = mapping.getHwQubits(opTargets); + auto targetCoords = hardwareQubits.getCoordIndices(targetHwQubits); + const auto opControls = pbComb.op->getControls(); + HwQubitsVector controlQubits; + for (const auto& control : opControls) { + controlQubits.emplace_back(control.qubit); + } + const auto controlHwQubits = mapping.getHwQubits(controlQubits); + auto controlCoords = hardwareQubits.getCoordIndices(controlHwQubits); + + for (const auto& passBy : pbComb.moves) { + mappedQc.move(passBy.c1, passBy.c2 + arch->getNpositions()); + if (this->parameters.verbose) { + spdlog::info("passby {} {}", passBy.c1, passBy.c2); + } + auto itT = std::ranges::find(targetCoords, passBy.c1); + if (itT != targetCoords.end()) { + *itT = passBy.c2 + arch->getNpositions(); + } + auto itC = std::ranges::find(controlCoords, passBy.c1); + if (itC != controlCoords.end()) { + *itC = passBy.c2 + arch->getNpositions(); + } + } + const auto opCopy = pbComb.op->clone(); + opCopy->setTargets(targetCoords); + qc::Controls controls; + for (const auto& control : controlCoords) { + controls.emplace(control); + } + opCopy->setControls(controls); + mappedQc.emplace_back(opCopy->clone()); + + // mapGate(faComb.op); + for (const auto& passBy : pbComb.moves) { + mappedQc.move(passBy.c2 + arch->getNpositions(), passBy.c1); + if (this->parameters.verbose) { + spdlog::info("passby {} {}", passBy.c2, passBy.c1); + } + } + + frontLayer.removeGatesAndUpdate({pbComb.op}); + this->frontLayerShuttling.erase( + std::ranges::find(this->frontLayerShuttling, pbComb.op)); + stats.nPassBy += pbComb.moves.size(); +} + void NeutralAtomMapper::reassignGatesToLayers(const GateList& frontGates, const GateList& lookaheadGates) { // assign gates to gates or shuttling this->frontLayerGate.clear(); this->frontLayerShuttling.clear(); for (const auto& gate : frontGates) { + if (gate->getNqubits() == 1) { + continue; + } if (swapGateBetter(gate)) { this->frontLayerGate.emplace_back(gate); } else { @@ -177,6 +251,9 @@ void NeutralAtomMapper::reassignGatesToLayers(const GateList& frontGates, this->lookaheadLayerGate.clear(); this->lookaheadLayerShuttling.clear(); for (const auto& gate : lookaheadGates) { + if (gate->getNqubits() == 1) { + continue; + } if (swapGateBetter(gate)) { this->lookaheadLayerGate.emplace_back(gate); } else { @@ -186,24 +263,14 @@ void NeutralAtomMapper::reassignGatesToLayers(const GateList& frontGates, } void NeutralAtomMapper::mapGate(const qc::Operation* op) { - if (op->getType() == qc::OpType::I) { - return; - } - // Safety check - if (std::ranges::find(this->executedCommutingGates, op) != - this->executedCommutingGates.end()) { - return; - } - this->executedCommutingGates.emplace_back(op); if (this->parameters.verbose) { - std::cout << "mapped " << op->getName() << " "; - for (auto qubit : op->getUsedQubits()) { - std::cout << qubit << " "; + spdlog::info("mapped {}", op->getName()); + for (const auto qubit : op->getUsedQubits()) { + spdlog::info("{}", qubit); } - std::cout << "\n"; } // convert circuit qubits to CoordIndex and append to mappedQc - auto opCopyUnique = op->clone(); + const auto opCopyUnique = op->clone(); auto* opCopy = opCopyUnique.get(); this->mapping.mapToHwQubits(opCopy); this->hardwareQubits.mapToCoordIdx(opCopy); @@ -211,131 +278,222 @@ void NeutralAtomMapper::mapGate(const qc::Operation* op) { } bool NeutralAtomMapper::isExecutable(const qc::Operation* opPointer) { - auto usedQubits = opPointer->getUsedQubits(); - auto nUsedQubits = usedQubits.size(); - if (nUsedQubits == 1) { - return true; - } + const auto usedQubits = opPointer->getUsedQubits(); std::set usedHwQubits; - for (auto qubit : usedQubits) { + for (const auto qubit : usedQubits) { usedHwQubits.emplace(this->mapping.getHwQubit(qubit)); } return this->hardwareQubits.getAllToAllSwapDistance(usedHwQubits) == 0; } -void NeutralAtomMapper::printLayers() { - std::cout << "f,g: "; +void NeutralAtomMapper::printLayers() const { + spdlog::info("f,g:"); for (const auto* op : this->frontLayerGate) { - std::cout << op->getName() << " "; - for (auto qubit : op->getUsedQubits()) { - std::cout << qubit << " "; + spdlog::info("{}", op->getName()); + for (const auto qubit : op->getUsedQubits()) { + spdlog::info("{}", qubit); } - std::cout << '\n'; } - std::cout << "f,s: "; + spdlog::info(""); + spdlog::info("f,s:"); for (const auto* op : this->frontLayerShuttling) { - std::cout << op->getName() << " "; - for (auto qubit : op->getUsedQubits()) { - std::cout << qubit << " "; + spdlog::info("{}", op->getName()); + for (const auto qubit : op->getUsedQubits()) { + spdlog::info("{}", qubit); } - std::cout << '\n'; } - std::cout << "l,g: "; + spdlog::info(""); + spdlog::info("l,g:"); for (const auto* op : this->lookaheadLayerGate) { - std::cout << op->getName() << " "; - for (auto qubit : op->getUsedQubits()) { - std::cout << qubit << " "; + spdlog::info("{}", op->getName()); + for (const auto qubit : op->getUsedQubits()) { + spdlog::info("{}", qubit); } - std::cout << '\n'; } - std::cout << '\n'; - std::cout << "l,g: "; + spdlog::info(""); + spdlog::info("l,s:"); for (const auto* op : this->lookaheadLayerShuttling) { - std::cout << op->getName() << " "; - for (auto qubit : op->getUsedQubits()) { - std::cout << qubit << " "; + spdlog::info("{}", op->getName()); + for (const auto qubit : op->getUsedQubits()) { + spdlog::info("{}", qubit); } - std::cout << '\n'; } - std::cout << '\n'; + spdlog::info(""); } GateList NeutralAtomMapper::getExecutableGates(const GateList& gates) { GateList executableGates; for (const auto* opPointer : gates) { - if (isExecutable(opPointer)) { + if (opPointer->getNqubits() == 1 || isExecutable(opPointer)) { executableGates.emplace_back(opPointer); } } return executableGates; } -void NeutralAtomMapper::updateMappingSwap(Swap swap) { - nSwaps++; +void NeutralAtomMapper::updateBlockedQubits(const HwQubits& qubits) { // save to lastSwaps this->lastBlockedQubits.emplace_back( - this->hardwareQubits.getBlockedQubits({swap.first, swap.second})); - if (this->lastBlockedQubits.size() > this->arch.getNcolumns()) { + this->hardwareQubits.getBlockedQubits(qubits)); + if (this->lastBlockedQubits.size() > this->arch->getNcolumns()) { this->lastBlockedQubits.pop_front(); } +} + +void NeutralAtomMapper::applySwap(const Swap& swap) { + stats.nSwaps++; + this->mapping.applySwap(swap); // convert circuit qubits to CoordIndex and append to mappedQc - auto idxFirst = this->hardwareQubits.getCoordIndex(swap.first); - auto idxSecond = this->hardwareQubits.getCoordIndex(swap.second); + const auto idxFirst = this->hardwareQubits.getCoordIndex(swap.first); + const auto idxSecond = this->hardwareQubits.getCoordIndex(swap.second); this->mappedQc.swap(idxFirst, idxSecond); if (this->parameters.verbose) { - std::cout << "swapped " << swap.first << " " << swap.second; - std::cout << " logical qubits: "; + spdlog::info("swapped {} {}", swap.first, swap.second); + spdlog::info(" logical qubits:"); if (this->mapping.isMapped(swap.first)) { - std::cout << this->mapping.getCircQubit(swap.first); + spdlog::info("{}", this->mapping.getCircQubit(swap.first)); } else { - std::cout << "not mapped"; + spdlog::info("not mapped"); } if (this->mapping.isMapped(swap.second)) { - std::cout << " " << this->mapping.getCircQubit(swap.second); + spdlog::info(" {}", this->mapping.getCircQubit(swap.second)); } else { - std::cout << " not mapped"; + spdlog::info(" not mapped"); } - std::cout << '\n'; } } -void NeutralAtomMapper::updateMappingMove(AtomMove move) { +void NeutralAtomMapper::applyMove(AtomMove move) { this->lastMoves.emplace_back(move); if (this->lastMoves.size() > 4) { this->lastMoves.pop_front(); } - mappedQc.move(move.first, move.second); - auto toMoveHwQubit = this->hardwareQubits.getHwQubit(move.first); - this->hardwareQubits.move(toMoveHwQubit, move.second); + mappedQc.move(move.c1, move.c2); + const auto toMoveHwQubit = this->hardwareQubits.getHwQubit(move.c1); + this->hardwareQubits.move(toMoveHwQubit, move.c2); if (this->parameters.verbose) { - std::cout << "moved " << move.first << " to " << move.second; + spdlog::info("moved {} to {}", move.c1, move.c2); if (this->mapping.isMapped(toMoveHwQubit)) { - std::cout << " logical qubit: " - << this->mapping.getCircQubit(toMoveHwQubit) << '\n'; + spdlog::info(" logical qubit: {}", + this->mapping.getCircQubit(toMoveHwQubit)); } else { - std::cout << " not mapped" << '\n'; + spdlog::info(" not mapped"); + } + } + stats.nMoves++; +} +void NeutralAtomMapper::applyBridge(NeutralAtomLayer& frontLayer, + const Bridge& bridge) { + const auto coordIndices = this->hardwareQubits.getCoordIndices(bridge.second); + mappedQc.bridge(coordIndices); + + if (this->parameters.verbose) { + spdlog::info("bridged {}", bridge.first->getName()); + for (const auto qubit : bridge.second) { + spdlog::info("{}", qubit); + } + } + + // // remove gate from frontLayer + const auto* op = bridge.first; + frontLayer.removeGatesAndUpdate({op}); + this->frontLayerGate.erase(std::ranges::find(this->frontLayerGate, op)); + + stats.nBridges++; +} +void NeutralAtomMapper::applyFlyingAncilla(NeutralAtomLayer& frontLayer, + const FlyingAncillaComb& faComb) { + auto targetCoords = hardwareQubits.getCoordIndices( + mapping.getHwQubits(faComb.op->getTargets())); + // get control vector + const auto opControls = faComb.op->getControls(); + HwQubitsVector controlQubits; + for (const auto& control : opControls) { + controlQubits.emplace_back(control.qubit); + } + auto controlCoords = + hardwareQubits.getCoordIndices(mapping.getHwQubits(controlQubits)); + // merge target and control coords + auto allCoords = targetCoords; + allCoords.insert(allCoords.end(), controlCoords.begin(), controlCoords.end()); + + uint32_t i = 0; + const auto nPos = this->arch->getNpositions(); + for (const auto& passBy : faComb.moves) { + const auto ancQ1 = passBy.q1 + (nPos * 2); + const auto ancQ2 = passBy.q2 + (nPos * 2); + mappedQc.move(passBy.origin + 2 * nPos, ancQ1); + mappedQc.h(ancQ1); + mappedQc.cz(allCoords[i], ancQ1); + mappedQc.h(ancQ1); + mappedQc.move(ancQ1, ancQ2); + + auto itT = std::ranges::find(targetCoords, allCoords[i]); + if (itT != targetCoords.end()) { + *itT = ancQ2; + } + auto itC = std::ranges::find(controlCoords, allCoords[i]); + if (itC != controlCoords.end()) { + *itC = ancQ2; + } + i += 2; + + if (this->parameters.verbose) { + spdlog::info("passby (flying ancilla) {} {} {}", passBy.origin, passBy.q1, + passBy.q2); + } + } + const auto opCopy = faComb.op->clone(); + opCopy->setTargets(targetCoords); + qc::Controls controls; + for (const auto& control : controlCoords) { + controls.emplace(control); + } + opCopy->setControls(controls); + mappedQc.emplace_back(opCopy->clone()); + + i = 0; + for (const auto& passBy : faComb.moves) { + const auto ancQ1 = passBy.q1 + (nPos * 2); + const auto ancQ2 = passBy.q2 + (nPos * 2); + mappedQc.move(ancQ2, ancQ1); + mappedQc.h(ancQ1); + mappedQc.cz(allCoords[i], ancQ1); + i += 2; + mappedQc.h(ancQ1); + + // update position of flying ancillas + if (passBy.q1 != passBy.origin) { + this->flyingAncillas.move(static_cast(passBy.index), passBy.q1); + } + + if (this->parameters.verbose) { + spdlog::info("passby (flying ancilla) {} {}", passBy.q2, passBy.q1); } } - nMoves++; + + frontLayer.removeGatesAndUpdate({faComb.op}); + this->frontLayerShuttling.erase( + std::ranges::find(this->frontLayerShuttling, faComb.op)); + stats.nFAncillas += faComb.moves.size(); } -Swap NeutralAtomMapper::findBestSwap(const Swap& lastSwap) { +Swap NeutralAtomMapper::findBestSwap(const Swap& lastSwapUsed) { // compute necessary movements - auto swapsFront = initSwaps(this->frontLayerGate); - auto swapsLookahead = initSwaps(this->lookaheadLayerGate); + const auto swapsFront = initSwaps(this->frontLayerGate); + const auto swapsLookahead = initSwaps(this->lookaheadLayerGate); setTwoQubitSwapWeight(swapsFront.second); // evaluate swaps based on cost function auto swaps = getAllPossibleSwaps(swapsFront); // remove last swap to prevent immediate swap back - swaps.erase(lastSwap); - swaps.erase({lastSwap.second, lastSwap.first}); + swaps.erase(lastSwapUsed); + swaps.erase({lastSwapUsed.second, lastSwapUsed.first}); // no swap possible if (swaps.empty()) { - return {std::numeric_limits::max(), - std::numeric_limits::max()}; + return {}; } std::vector> swapCosts; swapCosts.reserve(swaps.size()); @@ -346,7 +504,7 @@ Swap NeutralAtomMapper::findBestSwap(const Swap& lastSwap) { return swap1.second < swap2.second; }); // get swap of minimal cost - auto bestSwap = std::ranges::min_element( + const auto bestSwap = std::ranges::min_element( swapCosts, [](const auto& swap1, const auto& swap2) { return swap1.second < swap2.second; }); @@ -354,7 +512,7 @@ Swap NeutralAtomMapper::findBestSwap(const Swap& lastSwap) { } void NeutralAtomMapper::setTwoQubitSwapWeight(const WeightedSwaps& swapExact) { - for (const auto& [swap, weight] : swapExact) { + for (const auto& weight : swapExact | std::views::values) { this->twoQubitSwapWeight = std::min(weight, this->twoQubitSwapWeight); } } @@ -375,7 +533,7 @@ std::set NeutralAtomMapper::getAllPossibleSwaps( swaps.emplace(swapSecond); } } - for (const auto& [swap, weight] : swapExactFront) { + for (const auto& swap : swapExactFront | std::views::keys) { const auto nearbySwapsFirst = this->hardwareQubits.getNearbySwaps(swap.first); for (const auto& swapFirst : nearbySwapsFirst) { @@ -384,6 +542,163 @@ std::set NeutralAtomMapper::getAllPossibleSwaps( } return swaps; } +Bridge NeutralAtomMapper::findBestBridge(const Swap& bestSwap) { + auto allBridges = getShortestBridges(bestSwap); + if (allBridges.empty()) { + return {}; + } + if (allBridges.size() == 1) { + return allBridges.front(); + } + // use bridge along less used qubits + const auto qubitUsages = computeCurrentCoordUsages(); + size_t bestBridgeIdx = 0; + size_t minUsage = std::numeric_limits::max(); + for (size_t i = 0; i < allBridges.size(); ++i) { + size_t usage = 0; + for (const auto qubit : allBridges[i].second) { + usage += qubitUsages[hardwareQubits.getCoordIndex(qubit)]; + } + if (usage < minUsage) { + minUsage = usage; + bestBridgeIdx = i; + } + } + return allBridges[bestBridgeIdx]; +} + +Bridges NeutralAtomMapper::getShortestBridges(const Swap& bestSwap) { + Bridges allBridges; + size_t minBridgeLength = std::numeric_limits::max(); + for (const auto* const op : this->frontLayerGate) { + if (op->getUsedQubits().size() == 2) { + // only consider gates which involve at least one of the swapped qubits + auto usedQuBits = op->getUsedQubits(); + auto usedHwQubits = this->mapping.getHwQubits(usedQuBits); + if (!usedHwQubits.contains(bestSwap.first) && + !usedHwQubits.contains(bestSwap.second) && bestSwap != Swap()) { + continue; + } + // shortcut if distance already larger than minBridgeLength + const auto dist = + this->hardwareQubits.getAllToAllSwapDistance(usedHwQubits); + if (dist > this->parameters.maxBridgeDistance || + dist > static_cast(minBridgeLength)) { + continue; + } + const auto bridges = this->hardwareQubits.computeAllShortestPaths( + *usedHwQubits.begin(), *usedHwQubits.rbegin()); + if (bridges.empty()) { + continue; + } + if (bridges.front().size() < minBridgeLength) { + minBridgeLength = bridges.front().size(); + allBridges.clear(); + } + for (const auto& bridge : bridges) { + if (bridge.size() == minBridgeLength) { + allBridges.emplace_back(op, bridge); + } + } + } + } + return allBridges; +} +CoordIndices NeutralAtomMapper::computeCurrentCoordUsages() const { + // Size to cover all register spaces: logical + ancilla + flying ancilla + CoordIndices coordUsages(static_cast(arch->getNpositions() * 3U), + 0); + // in front layer + for (const auto* const op : this->frontLayerGate) { + for (const auto qubit : op->getUsedQubits()) { + coordUsages[hardwareQubits.getCoordIndex(mapping.getHwQubit(qubit))]++; + } + } + // in mapped qc, go backwards same length as front layer + auto nFrontLayerGates = this->frontLayerGate.size(); + auto it = this->mappedQc.rbegin(); + while (it != this->mappedQc.rend() && nFrontLayerGates > 0) { + for (const auto coordIdx : (*it)->getUsedQubits()) { + coordUsages[coordIdx]++; + } + ++it; + nFrontLayerGates--; + } + // add last blocked qubits + if (this->lastBlockedQubits.empty()) { + return coordUsages; + } + const auto lastBlockedQubitSet = this->lastBlockedQubits.back(); + for (const auto qubit : lastBlockedQubitSet) { + coordUsages[hardwareQubits.getCoordIndex(qubit)]++; + } + return coordUsages; +} +FlyingAncillaComb NeutralAtomMapper::convertMoveCombToFlyingAncillaComb( + const MoveComb& moveComb) const { + if (this->flyingAncillas.getNumQubits() == 0) { + return {}; + } + const auto usedQubits = moveComb.op->getUsedQubits(); + const auto hwQubits = this->mapping.getHwQubits(usedQubits); + const auto usedCoords = this->hardwareQubits.getCoordIndices(hwQubits); + + // multi-qubit gate -> only one direction + std::vector bestFAs; + FlyingAncilla bestFA{}; + HwQubits usedFA; + for (const auto move : moveComb.moves) { + if (usedCoords.contains(move.c1)) { + const auto nearFirstIdx = + this->flyingAncillas.getClosestQubit(move.c1, usedFA); + const auto nearFirst = this->flyingAncillas.getCoordIndex(nearFirstIdx); + const auto nearSecondIdx = + this->flyingAncillas.getClosestQubit(move.c2, usedFA); + const auto nearSecond = this->flyingAncillas.getCoordIndex(nearSecondIdx); + if (usedQubits.size() == 2) { + // both directions possible, check if reversed is better + if (this->arch->getEuclideanDistance(nearFirst, move.c1) < + this->arch->getEuclideanDistance(nearSecond, move.c2)) { + bestFA.q1 = move.c2; + bestFA.q2 = move.c1; + bestFA.origin = nearSecond; + bestFA.index = nearSecondIdx; + + usedFA.emplace(bestFA.index); + bestFAs.emplace_back(bestFA); + continue; + } + } + bestFA.q1 = move.c1; + bestFA.q2 = move.c2; + bestFA.origin = nearFirst; + bestFA.index = nearFirstIdx; + + usedFA.emplace(bestFA.index); + bestFAs.emplace_back(bestFA); + } + } + return {.moves = bestFAs, .op = moveComb.op}; +} + +PassByComb +NeutralAtomMapper::convertMoveCombToPassByComb(const MoveComb& moveComb) const { + if (!this->parameters.usePassBy) { + return {}; + } + const auto usedQubits = moveComb.op->getUsedQubits(); + const auto hwQubits = this->mapping.getHwQubits(usedQubits); + const auto usedCoords = this->hardwareQubits.getCoordIndices(hwQubits); + + std::vector bestPbs; + for (const auto move : moveComb.moves) { + if (usedCoords.contains(move.c1)) { + bestPbs.emplace_back(AtomMove{ + .c1 = move.c1, .c2 = move.c2, .load1 = true, .load2 = false}); + } + } + return PassByComb{.moves = bestPbs, .op = moveComb.op}; +} qc::fp NeutralAtomMapper::swapCost( const Swap& swap, const std::pair& swapsFront, @@ -391,7 +706,7 @@ qc::fp NeutralAtomMapper::swapCost( auto [swapCloseByFront, swapExactFront] = swapsFront; auto [swapCloseByLookahead, swapExactLookahead] = swapsLookahead; // compute the change in total distance - auto distanceChangeFront = + const auto distanceChangeFront = swapCostPerLayer(swap, swapCloseByFront, swapExactFront) / static_cast(this->frontLayerGate.size()); qc::fp distanceChangeLookahead = 0; @@ -400,7 +715,8 @@ qc::fp NeutralAtomMapper::swapCost( swapCostPerLayer(swap, swapCloseByLookahead, swapExactLookahead) / static_cast(this->lookaheadLayerGate.size()); } - auto cost = parameters.lookaheadWeightSwaps * distanceChangeLookahead + + auto cost = (parameters.lookaheadWeightSwaps * distanceChangeLookahead / + this->parameters.lookaheadDepth) + distanceChangeFront; // compute the last time one of the swap qubits was used if (this->parameters.decay != 0) { @@ -416,6 +732,56 @@ qc::fp NeutralAtomMapper::swapCost( } return cost; } +qc::fp NeutralAtomMapper::swapDistanceReduction(const Swap& swap, + const GateList& layer) { + qc::fp swapDistReduction = 0; + for (const auto& op : layer) { + auto usedQubits = op->getUsedQubits(); + auto hwQubits = this->mapping.getHwQubits(usedQubits); + const auto& distBefore = + this->hardwareQubits.getAllToAllSwapDistance(hwQubits); + const auto firstPos = hwQubits.find(swap.first); + const auto secondPos = hwQubits.find(swap.second); + if (firstPos != hwQubits.end() && secondPos != hwQubits.end()) { + continue; + } + if (firstPos != hwQubits.end()) { + hwQubits.erase(firstPos); + hwQubits.insert(swap.second); + } + if (secondPos != hwQubits.end()) { + hwQubits.erase(secondPos); + hwQubits.insert(swap.first); + } + const auto& distAfter = + this->hardwareQubits.getAllToAllSwapDistance(hwQubits); + swapDistReduction += distBefore - distAfter; + } + return swapDistReduction; +} + +qc::fp +NeutralAtomMapper::moveCombDistanceReduction(const MoveComb& moveComb, + const GateList& layer) const { + qc::fp moveDistReduction = 0; + for (const auto& op : layer) { + auto usedQubits = op->getUsedQubits(); + auto hwQubits = this->mapping.getHwQubits(usedQubits); + auto coordIndices = this->hardwareQubits.getCoordIndices(hwQubits); + for (const auto& move : moveComb.moves) { + if (coordIndices.contains(move.c1)) { + const auto& distBefore = + this->arch->getAllToAllEuclideanDistance(coordIndices); + coordIndices.erase(move.c1); + coordIndices.insert(move.c2); + const auto& distAfter = + this->arch->getAllToAllEuclideanDistance(coordIndices); + moveDistReduction += distBefore - distAfter; + } + } + } + return moveDistReduction; +} std::pair NeutralAtomMapper::initSwaps(const GateList& layer) { @@ -432,11 +798,10 @@ NeutralAtomMapper::initSwaps(const GateList& layer) { // for multi-qubit gates, find the best position around the gate qubits auto bestPos = getBestMultiQubitPosition(gate); if (this->parameters.verbose) { - std::cout << "bestPos: "; - for (auto qubit : bestPos) { - std::cout << qubit << " "; + spdlog::info("bestPos:"); + for (const auto qubit : bestPos) { + spdlog::info("{}", qubit); } - std::cout << '\n'; } // then compute the exact moves to get to the best position auto exactSwapsToPos = getExactSwapsToPosition(gate, bestPos); @@ -486,8 +851,8 @@ qc::fp NeutralAtomMapper::swapCostPerLayer(const Swap& swap, // move qubits to the exact position for multi-qubit gates for (const auto& [exactSwap, weight] : swapExact) { - auto origin = exactSwap.first; - auto destination = exactSwap.second; + const auto origin = exactSwap.first; + const auto destination = exactSwap.second; distBefore = this->hardwareQubits.getSwapDistance(origin, destination, false); if (distBefore == std::numeric_limits::max()) { @@ -518,7 +883,8 @@ qc::fp NeutralAtomMapper::swapCostPerLayer(const Swap& swap, return distChange; } -HwQubits NeutralAtomMapper::getBestMultiQubitPosition(const qc::Operation* op) { +HwQubits +NeutralAtomMapper::getBestMultiQubitPosition(const qc::Operation* opPointer) { // try to find position around gate Qubits recursively // if not, search through coupling graph until found according to a // priority queue based on the distance to the other qubits @@ -527,8 +893,8 @@ HwQubits NeutralAtomMapper::getBestMultiQubitPosition(const qc::Operation* op) { std::vector>, std::greater<>> qubitQueue; // add the gate qubits to the priority queue - auto gateQubits = op->getUsedQubits(); - auto gateHwQubits = this->mapping.getHwQubits(gateQubits); + const auto gateQubits = opPointer->getUsedQubits(); + const auto gateHwQubits = this->mapping.getHwQubits(gateQubits); // add the gate qubits to the priority queue for (const auto& gateQubit : gateHwQubits) { qc::fp totalDist = 0; @@ -578,16 +944,17 @@ HwQubits NeutralAtomMapper::getBestMultiQubitPosition(const qc::Operation* op) { } } // find gate and move it to the shuttling layer - auto idxFrontGate = std::ranges::find(this->frontLayerGate, op); + const auto idxFrontGate = std::ranges::find(this->frontLayerGate, opPointer); if (idxFrontGate != this->frontLayerGate.end()) { this->frontLayerGate.erase(idxFrontGate); - this->frontLayerShuttling.emplace_back(op); + this->frontLayerShuttling.emplace_back(opPointer); } // remove from lookahead layer if there - auto idxLookaheadGate = std::ranges::find(this->lookaheadLayerGate, op); + const auto idxLookaheadGate = + std::ranges::find(this->lookaheadLayerGate, opPointer); if (idxLookaheadGate != this->lookaheadLayerGate.end()) { this->lookaheadLayerGate.erase(idxLookaheadGate); - this->lookaheadLayerShuttling.emplace_back(op); + this->lookaheadLayerShuttling.emplace_back(opPointer); } return {}; } @@ -601,7 +968,7 @@ HwQubits NeutralAtomMapper::getBestMultiQubitPositionRec( return bestPos; } // update remainingNearbyQubits - auto newQubit = *selectedQubits.rbegin(); + const auto newQubit = *selectedQubits.rbegin(); auto nearbyNextQubit = this->hardwareQubits.getNearbyQubits(newQubit); // compute remaining qubits as the intersection with current Qubits newRemainingQubits; @@ -637,7 +1004,7 @@ HwQubits NeutralAtomMapper::getBestMultiQubitPositionRec( summedDistances.emplace_back(hwQubit, distance); } // select next qubit as the one with minimal distance - auto nextQubitDist = std::ranges::min_element( + const auto nextQubitDist = std::ranges::min_element( summedDistances, [](const auto& qubit1, const auto& qubit2) { return qubit1.second < qubit2.second; }); @@ -648,7 +1015,7 @@ HwQubits NeutralAtomMapper::getBestMultiQubitPositionRec( auto closesDistance = this->hardwareQubits.getSwapDistance(closesGateQubits, nextQubit, true); for (const auto& gateQubit : remainingGateQubits) { - auto distance = + const auto distance = this->hardwareQubits.getSwapDistance(gateQubit, nextQubit, true); if (distance < closesDistance) { closesGateQubits = gateQubit; @@ -664,10 +1031,7 @@ HwQubits NeutralAtomMapper::getBestMultiQubitPositionRec( WeightedSwaps NeutralAtomMapper::getExactSwapsToPosition(const qc::Operation* op, HwQubits position) { - if (position.empty()) { - return {}; - } - auto gateQubits = op->getUsedQubits(); + const auto gateQubits = op->getUsedQubits(); auto gateHwQubits = this->mapping.getHwQubits(gateQubits); WeightedSwaps swapsExact; while (!position.empty() && !gateHwQubits.empty()) { @@ -677,7 +1041,7 @@ NeutralAtomMapper::getExactSwapsToPosition(const qc::Operation* op, for (const auto& gateQubit : gateHwQubits) { SwapDistance minimalDistance = std::numeric_limits::max(); for (const auto& posQubit : position) { - auto distance = + const auto distance = this->hardwareQubits.getSwapDistance(gateQubit, posQubit, false); if (distance < minimalDistance) { minimalDistance = distance; @@ -687,22 +1051,6 @@ NeutralAtomMapper::getExactSwapsToPosition(const qc::Operation* op, minimalDistancePosQubit.emplace(posQubit); } } - if (minimalDistance == std::numeric_limits::max()) { - // not possible to move to position - // move gate to shuttling layer - auto idxFrontGate = std::ranges::find(this->frontLayerGate, op); - if (idxFrontGate != this->frontLayerGate.end()) { - this->frontLayerGate.erase(idxFrontGate); - this->frontLayerShuttling.emplace_back(op); - } - // remove from lookahead layer if there - auto idxLookaheadGate = std::ranges::find(this->lookaheadLayerGate, op); - if (idxLookaheadGate != this->lookaheadLayerGate.end()) { - this->lookaheadLayerGate.erase(idxLookaheadGate); - this->lookaheadLayerShuttling.emplace_back(op); - } - return {}; - } minimalDistances.emplace_back(gateQubit, minimalDistancePosQubit, minimalDistance); } @@ -754,23 +1102,23 @@ NeutralAtomMapper::getExactSwapsToPosition(const qc::Operation* op, // compute total distance of all moves SwapDistance totalDistance = 0; - for (const auto& [swap, weight] : swapsExact) { + for (const auto& swap : swapsExact | std::views::keys) { auto [q1, q2] = swap; totalDistance += this->hardwareQubits.getSwapDistance(q1, q2, false); } // add cost to the moves -> move first qubit corresponding to almost finished // positions - auto nQubits = op->getUsedQubits().size(); - auto multiQubitFactor = + const auto nQubits = op->getUsedQubits().size(); + const auto multiQubitFactor = (static_cast(nQubits) * static_cast(nQubits - 1)) / 2; - for (auto& move : swapsExact) { - move.second = multiQubitFactor / static_cast(totalDistance); + for (auto& val : swapsExact | std::views::values) { + val = multiQubitFactor / static_cast(totalDistance); } return swapsExact; } -AtomMove NeutralAtomMapper::findBestAtomMove() { +MoveComb NeutralAtomMapper::findBestAtomMove() { auto moveCombs = getAllMoveCombinations(); // compute cost for each move combination @@ -785,129 +1133,77 @@ AtomMove NeutralAtomMapper::findBestAtomMove() { }); // get move of minimal cost - auto bestMove = std::ranges::min_element( + const auto bestMove = std::ranges::min_element( moveCosts, [](const auto& move1, const auto& move2) { return move1.second < move2.second; }); - return bestMove->first.getFirstMove(); + return bestMove->first; } -qc::fp NeutralAtomMapper::moveCostComb(const MoveComb& moveComb) { +qc::fp NeutralAtomMapper::moveCostComb(const MoveComb& moveComb) const { qc::fp costComb = 0; - for (const auto& move : moveComb.moves) { - costComb += moveCost(move); - } - return costComb; -} - -qc::fp NeutralAtomMapper::moveCost(const AtomMove& move) { - qc::fp cost = 0; - auto frontCost = moveCostPerLayer(move, this->frontLayerShuttling) / - static_cast(this->frontLayerShuttling.size()); - cost += frontCost; + const auto frontDistReduction = + moveCombDistanceReduction(moveComb, this->frontLayerShuttling) / + static_cast(this->frontLayerShuttling.size()); + costComb -= frontDistReduction; if (!lookaheadLayerShuttling.empty()) { - auto lookaheadCost = - moveCostPerLayer(move, this->lookaheadLayerShuttling) / + const auto lookaheadDistReduction = + moveCombDistanceReduction(moveComb, this->lookaheadLayerShuttling) / static_cast(this->lookaheadLayerShuttling.size()); - cost += parameters.lookaheadWeightMoves * lookaheadCost; + costComb -= parameters.lookaheadWeightMoves * lookaheadDistReduction / + static_cast(this->parameters.lookaheadDepth); } if (!this->lastMoves.empty()) { - auto parallelCost = parameters.shuttlingTimeWeight * - parallelMoveCost(move) / - static_cast(this->lastMoves.size()) / - static_cast(this->frontLayerShuttling.size()); - cost += parallelCost; + const auto parallelMovecCost = + parameters.shuttlingTimeWeight * parallelMoveCost(moveComb) / + static_cast(this->frontLayerShuttling.size()); + costComb += parallelMovecCost; } - - return cost; -} - -qc::fp NeutralAtomMapper::moveCostPerLayer(const AtomMove& move, - GateList& layer) { - // compute cost assuming the move was applied - qc::fp distChange = 0; - auto toMoveHwQubit = this->hardwareQubits.getHwQubit(move.first); - if (this->mapping.isMapped(toMoveHwQubit)) { - auto toMoveCircuitQubit = this->mapping.getCircQubit(toMoveHwQubit); - for (const auto& gate : layer) { - auto usedQubits = gate->getUsedQubits(); - if (usedQubits.contains(toMoveCircuitQubit)) { - // check distance reduction - qc::fp distanceBefore = 0; - for (const auto& qubit : usedQubits) { - if (qubit == toMoveCircuitQubit) { - continue; - } - auto hwQubit = this->mapping.getHwQubit(qubit); - auto dist = this->arch.getEuclideanDistance( - this->hardwareQubits.getCoordIndex(hwQubit), - this->hardwareQubits.getCoordIndex(toMoveHwQubit)); - distanceBefore += dist; - } - qc::fp distanceAfter = 0; - for (const auto& qubit : usedQubits) { - if (qubit == toMoveCircuitQubit) { - continue; - } - auto hwQubit = this->mapping.getHwQubit(qubit); - auto dist = this->arch.getEuclideanDistance( - this->hardwareQubits.getCoordIndex(hwQubit), move.second); - distanceAfter += dist; - } - distChange += distanceAfter - distanceBefore; - } - } - } - return distChange; + return costComb; } -qc::fp NeutralAtomMapper::parallelMoveCost(const AtomMove& move) { +qc::fp NeutralAtomMapper::parallelMoveCost(const MoveComb& moveComb) const { qc::fp parallelCost = 0; - auto moveVector = this->arch.getVector(move.first, move.second); - std::vector lastEndingCoords; - if (this->lastMoves.empty()) { - parallelCost += arch.getVectorShuttlingTime(moveVector); - } + // only first move matters for parallelization + const auto move = moveComb.moves.front(); + const auto moveVector = this->arch->getVector(move.c1, move.c2); + parallelCost += arch->getVectorShuttlingTime(moveVector); + bool canBeDoneInParallel = true; for (const auto& lastMove : this->lastMoves) { - lastEndingCoords.emplace_back(lastMove.second); // decide of shuttling can be done in parallel - auto lastMoveVector = this->arch.getVector(lastMove.first, lastMove.second); + auto lastMoveVector = this->arch->getVector(lastMove.c1, lastMove.c2); if (moveVector.overlap(lastMoveVector)) { if (moveVector.direction != lastMoveVector.direction) { - parallelCost += arch.getVectorShuttlingTime(moveVector); - } else { - // check if move can be done in parallel - if (moveVector.include(lastMoveVector)) { - parallelCost += arch.getVectorShuttlingTime(moveVector); - } + canBeDoneInParallel = false; + break; + } // check if move can be done in parallel + if (moveVector.include(lastMoveVector)) { + canBeDoneInParallel = false; + break; } } } + if (canBeDoneInParallel) { + parallelCost -= arch->getVectorShuttlingTime(moveVector); + } // check if in same row/column like last moves // then can may be loaded in parallel - auto moveCoordInit = this->arch.getCoordinate(move.first); - auto moveCoordEnd = this->arch.getCoordinate(move.second); - parallelCost += arch.getShuttlingTime(qc::OpType::AodActivate) + - arch.getShuttlingTime(qc::OpType::AodDeactivate); + const auto moveCoordInit = this->arch->getCoordinate(move.c1); + const auto moveCoordEnd = this->arch->getCoordinate(move.c2); + parallelCost += arch->getShuttlingTime(qc::OpType::AodActivate) + + arch->getShuttlingTime(qc::OpType::AodDeactivate); for (const auto& lastMove : this->lastMoves) { - auto lastMoveCoordInit = this->arch.getCoordinate(lastMove.first); - auto lastMoveCoordEnd = this->arch.getCoordinate(lastMove.second); + const auto lastMoveCoordInit = this->arch->getCoordinate(lastMove.c1); + const auto lastMoveCoordEnd = this->arch->getCoordinate(lastMove.c2); if (moveCoordInit.x == lastMoveCoordInit.x || moveCoordInit.y == lastMoveCoordInit.y) { - parallelCost -= arch.getShuttlingTime(qc::OpType::AodActivate); + parallelCost -= arch->getShuttlingTime(qc::OpType::AodActivate); } if (moveCoordEnd.x == lastMoveCoordEnd.x || moveCoordEnd.y == lastMoveCoordEnd.y) { - parallelCost -= arch.getShuttlingTime(qc::OpType::AodDeactivate); + parallelCost -= arch->getShuttlingTime(qc::OpType::AodDeactivate); } } - // check if move can use AOD atom from last moves - // if (std::find(lastEndingCoords.begin(), lastEndingCoords.end(), - // move.first) == - // lastEndingCoords.end()) { - // parallelCost += arch.getShuttlingTime(qc::OpType::AodActivate) + - // arch.getShuttlingTime(qc::OpType::AodDeactivate); - // } return parallelCost; } @@ -922,14 +1218,15 @@ NeutralAtomMapper::getMovePositionRec(MultiQubitMovePos currentPos, return {}; } - auto nearbyCoords = this->arch.getNearbyCoordinates(currentPos.coords.back()); + const auto nearbyCoords = + this->arch->getNearbyCoordinates(currentPos.coords.back()); // filter out coords that have a SWAP distance unequal to 0 to any of the // current qubits. Also sort out coords that are already in the vector std::vector filteredNearbyCoords; for (const auto& coord : nearbyCoords) { bool valid = true; for (const auto& qubit : currentPos.coords) { - if (this->arch.getSwapDistance(qubit, coord) != 0 || coord == qubit) { + if (this->arch->getSwapDistance(qubit, coord) != 0 || coord == qubit) { valid = false; break; } @@ -977,30 +1274,30 @@ NeutralAtomMapper::getMovePositionRec(MultiQubitMovePos currentPos, } for (const auto& gateCoord : occupiedGateCoords) { - MultiQubitMovePos nextPos = MultiQubitMovePos(currentPos); + auto nextPos = MultiQubitMovePos(currentPos); nextPos.coords.emplace_back(gateCoord); - auto bestPos = getMovePositionRec(nextPos, gateCoords, maxNMoves); - if (bestPos.coords.size() == gateCoords.size()) { + if (auto bestPos = getMovePositionRec(nextPos, gateCoords, maxNMoves); + bestPos.coords.size() == gateCoords.size()) { return bestPos; } } for (const auto& freeCoord : freeNearbyCoords) { - MultiQubitMovePos nextPos = MultiQubitMovePos(currentPos); + auto nextPos = MultiQubitMovePos(currentPos); nextPos.coords.emplace_back(freeCoord); nextPos.nMoves += 1; - auto bestPos = getMovePositionRec(nextPos, gateCoords, maxNMoves); - if (bestPos.coords.size() == gateCoords.size()) { + if (auto bestPos = getMovePositionRec(nextPos, gateCoords, maxNMoves); + bestPos.coords.size() == gateCoords.size()) { return bestPos; } } for (const auto& occCoord : occupiedNearbyCoords) { - MultiQubitMovePos nextPos = MultiQubitMovePos(currentPos); + auto nextPos = MultiQubitMovePos(currentPos); nextPos.coords.emplace_back(occCoord); nextPos.nMoves += 2; - auto bestPos = getMovePositionRec(nextPos, gateCoords, maxNMoves); - if (bestPos.coords.size() == gateCoords.size()) { + if (auto bestPos = getMovePositionRec(nextPos, gateCoords, maxNMoves); + bestPos.coords.size() == gateCoords.size()) { return bestPos; } } @@ -1015,11 +1312,22 @@ MoveCombs NeutralAtomMapper::getAllMoveCombinations() { auto usedQubits = op->getUsedQubits(); auto usedHwQubits = this->mapping.getHwQubits(usedQubits); auto usedCoordsSet = this->hardwareQubits.getCoordIndices(usedHwQubits); - auto usedCoords = - std::vector(usedCoordsSet.begin(), usedCoordsSet.end()); - auto bestPos = getBestMovePos(usedCoords); - auto moves = getMoveCombinationsToPosition(usedHwQubits, bestPos); - allMoves.addMoveCombs(moves); + auto usedCoords = std::vector(usedCoordsSet.begin(), usedCoordsSet.end()); + std::set bestPositions; + // iterate over all possible permutations of the usedCoords + // to find different best positions + std::ranges::sort(usedCoords); + do { + bestPositions.insert(getBestMovePos(usedCoords)); + } while (std::ranges::next_permutation(usedCoords).found); + for (const auto& bestPos : bestPositions) { + auto moves = getMoveCombinationsToPosition(usedHwQubits, bestPos); + moves.setOperation(op, bestPos); + if (allMoves.size() > this->parameters.limitShuttlingLayer) { + break; + } + allMoves.addMoveCombs(moves); + } } allMoves.removeLongerMoveCombs(); return allMoves; @@ -1027,7 +1335,6 @@ MoveCombs NeutralAtomMapper::getAllMoveCombinations() { CoordIndices NeutralAtomMapper::getBestMovePos(const CoordIndices& gateCoords) { size_t const maxMoves = gateCoords.size() * 2; - size_t const minMoves = gateCoords.size(); size_t nMovesGate = maxMoves; // do a breadth first search for the best position // start with the used coords @@ -1057,30 +1364,25 @@ CoordIndices NeutralAtomMapper::getBestMovePos(const CoordIndices& gateCoords) { currentPos.nMoves = 1; } auto bestPos = getMovePositionRec(currentPos, gateCoords, nMovesGate); - if (!bestPos.coords.empty() && bestPos.nMoves <= minMoves) { - return bestPos.coords; + if (!bestPos.coords.empty() && bestPos.nMoves < nMovesGate) { + nMovesGate = bestPos.nMoves; + finalBestPos = bestPos; } // min not yet reached, check nearby if (!bestPos.coords.empty()) { nMovesGate = std::min(nMovesGate, bestPos.nMoves); } - for (const auto& nearbyCoord : this->arch.getNearbyCoordinates(coord)) { - if (std::ranges::find(visited, nearbyCoord) == visited.end()) { - q.push(nearbyCoord); - } - } } - throw std::runtime_error( - "No move position found (check if enough free coords are available)"); + if (finalBestPos.coords.empty()) { + throw std::runtime_error( + "No move position found (check if enough free coords are available)"); + } + return finalBestPos.coords; } -MoveCombs -NeutralAtomMapper::getMoveCombinationsToPosition(HwQubits& gateQubits, - CoordIndices& position) { - if (position.empty()) { - throw std::invalid_argument("No position given"); - } +MoveCombs NeutralAtomMapper::getMoveCombinationsToPosition( + const HwQubits& gateQubits, const CoordIndices& position) const { // compute for each qubit the best position around it based on the cost of // the single move choose best one MoveCombs const moveCombinations; @@ -1105,6 +1407,8 @@ NeutralAtomMapper::getMoveCombinationsToPosition(HwQubits& gateQubits, } } + // save coords where atoms have been moved away to + CoordIndices movedAwayCoords = remainingCoords; while (!remainingGateCoords.empty()) { auto currentGateQubit = *remainingGateCoords.begin(); // compute costs and find best coord @@ -1112,50 +1416,55 @@ NeutralAtomMapper::getMoveCombinationsToPosition(HwQubits& gateQubits, for (const auto& remainingCoord : remainingCoords) { if (this->hardwareQubits.isMapped(remainingCoord)) { const auto moveAwayComb = getMoveAwayCombinations( - currentGateQubit, remainingCoord, remainingCoords); + currentGateQubit, remainingCoord, movedAwayCoords); for (const auto& moveAway : moveAwayComb) { auto cost = moveCostComb(moveAway); costs.emplace_back(remainingCoord, cost); } } else { - auto cost = moveCost({currentGateQubit, remainingCoord}); + MoveComb const moveCombNew( + {AtomMove{.c1 = currentGateQubit, .c2 = remainingCoord}}); + auto cost = moveCostComb(moveCombNew); costs.emplace_back(remainingCoord, cost); } } // find minimal cost - auto bestCost = std::ranges::min_element( + const auto bestCost = std::ranges::min_element( costs, [](const auto& cost1, const auto& cost2) { return cost1.second < cost2.second; }); - auto bestCoord = bestCost->first; - if (this->hardwareQubits.isMapped(bestCoord)) { - auto moveAwayComb = - getMoveAwayCombinations(currentGateQubit, bestCoord, remainingCoords); - for (const auto& moveAway : moveAwayComb) { - moveComb.append(moveAway); - } + auto targetCoord = bestCost->first; + if (this->hardwareQubits.isMapped(targetCoord)) { + auto moveAwayComb = getMoveAwayCombinations(currentGateQubit, targetCoord, + movedAwayCoords); + moveComb.append(moveAwayComb.moveCombs[0]); + movedAwayCoords.emplace_back(moveAwayComb.moveCombs[0].moves[0].c2); } else { - moveComb.append(AtomMove{currentGateQubit, bestCoord}); + moveComb.append(AtomMove{.c1 = currentGateQubit, + .c2 = targetCoord, + .load1 = true, + .load2 = true}); } remainingGateCoords.erase(currentGateQubit); - remainingCoords.erase(std::ranges::find(remainingCoords, bestCoord)); + remainingCoords.erase(std::ranges::find(remainingCoords, targetCoord)); } return MoveCombs({moveComb}); } -MoveCombs -NeutralAtomMapper::getMoveAwayCombinations(CoordIndex startCoord, - CoordIndex targetCoord, - const CoordIndices& excludedCoords) { +MoveCombs NeutralAtomMapper::getMoveAwayCombinations( + const CoordIndex startCoord, const CoordIndex targetCoord, + const CoordIndices& excludedCoords) const { MoveCombs moveCombinations; - auto const originalVector = this->arch.getVector(startCoord, targetCoord); + auto const originalVector = this->arch->getVector(startCoord, targetCoord); auto const originalDirection = originalVector.direction; // Find move away target in the same direction as the original move - auto moveAwayTargets = this->hardwareQubits.findClosestFreeCoord( + const auto moveAwayTargets = this->hardwareQubits.findClosestFreeCoord( targetCoord, originalDirection, excludedCoords); for (const auto& moveAwayTarget : moveAwayTargets) { - const AtomMove move = {startCoord, targetCoord}; - const AtomMove moveAway = {targetCoord, moveAwayTarget}; + const AtomMove move = { + .c1 = startCoord, .c2 = targetCoord, .load1 = true, .load2 = true}; + const AtomMove moveAway = { + .c1 = targetCoord, .c2 = moveAwayTarget, .load1 = true, .load2 = true}; moveCombinations.addMoveComb(MoveComb({moveAway, move})); } if (moveCombinations.empty()) { @@ -1164,26 +1473,63 @@ NeutralAtomMapper::getMoveAwayCombinations(CoordIndex startCoord, return moveCombinations; } +size_t NeutralAtomMapper::shuttlingBasedMapping( + NeutralAtomLayer& frontLayer, NeutralAtomLayer& lookaheadLayer, size_t i) { + while (!this->frontLayerShuttling.empty()) { + ++i; + if (this->parameters.verbose) { + spdlog::info("iteration {}", i); + } + auto bestComb = findBestAtomMove(); + MappingMethod bestMethod = MoveMethod; + if (!multiQubitGates) { + auto bestFaComb = convertMoveCombToFlyingAncillaComb(bestComb); + auto bestPbComb = convertMoveCombToPassByComb(bestComb); + bestMethod = + compareShuttlingAndFlyingAncilla(bestComb, bestFaComb, bestPbComb); + + switch (bestMethod) { + case MoveMethod: + // apply whole move combination at once + for (const auto& move : bestComb.moves) { + applyMove(move); + } + // applyMove(bestComb.moves[0]); + break; + case FlyingAncillaMethod: + applyFlyingAncilla(frontLayer, bestFaComb); + break; + case PassByMethod: + applyPassBy(frontLayer, bestPbComb); + break; + default: + break; + } + } else { + for (const auto& move : bestComb.moves) { + applyMove(move); + } + } + + mapAllPossibleGates(frontLayer, lookaheadLayer); + reassignGatesToLayers(frontLayer.getGates(), lookaheadLayer.getGates()); + if (this->parameters.verbose) { + printLayers(); + } + } + return i; +} + std::pair NeutralAtomMapper::estimateNumSwapGates(const qc::Operation* opPointer) { - auto usedQubits = opPointer->getUsedQubits(); - auto usedHwQubits = this->mapping.getHwQubits(usedQubits); + const auto usedQubits = opPointer->getUsedQubits(); + const auto usedHwQubits = this->mapping.getHwQubits(usedQubits); qc::fp minNumSwaps = 0; if (usedHwQubits.size() == 2) { - SwapDistance minDistance = std::numeric_limits::max(); - for (const auto& hwQubit : usedHwQubits) { - for (const auto& otherHwQubit : usedHwQubits) { - if (hwQubit == otherHwQubit) { - continue; - } - auto distance = - this->hardwareQubits.getSwapDistance(hwQubit, otherHwQubit); - minDistance = std::min(distance, minDistance); - } - } - minNumSwaps = minDistance; + minNumSwaps = this->hardwareQubits.getSwapDistance( + *usedHwQubits.begin(), *(usedHwQubits.rbegin()), true); } else { // multi-qubit gates - auto bestPos = getBestMultiQubitPosition(opPointer); + const auto bestPos = getBestMultiQubitPosition(opPointer); if (bestPos.empty()) { return {std::numeric_limits::max(), std::numeric_limits::max()}; @@ -1193,19 +1539,19 @@ NeutralAtomMapper::estimateNumSwapGates(const qc::Operation* opPointer) { return {std::numeric_limits::max(), std::numeric_limits::max()}; } - for (const auto& [swap, weight] : exactSwaps) { + for (const auto& swap : exactSwaps | std::views::keys) { auto [q1, q2] = swap; minNumSwaps += this->hardwareQubits.getSwapDistance(q1, q2, false); } } - const qc::fp minTime = minNumSwaps * this->arch.getGateTime("swap"); + const qc::fp minTime = minNumSwaps * this->arch->getGateTime("swap"); return {minNumSwaps, minTime}; } std::pair -NeutralAtomMapper::estimateNumMove(const qc::Operation* opPointer) { - auto usedQubits = opPointer->getUsedQubits(); - auto usedHwQubits = this->mapping.getHwQubits(usedQubits); +NeutralAtomMapper::estimateNumMove(const qc::Operation* opPointer) const { + const auto usedQubits = opPointer->getUsedQubits(); + const auto usedHwQubits = this->mapping.getHwQubits(usedQubits); auto usedCoords = this->hardwareQubits.getCoordIndices(usedHwQubits); // estimate the number of moves as: // compute distance between qubits @@ -1227,25 +1573,25 @@ NeutralAtomMapper::estimateNumMove(const qc::Operation* opPointer) { auto nearbyFreeIt = nearbyFreeCoords.begin(); auto nearbyOccIt = nearbyOccupiedCoords.begin(); while (otherQubitsIt != usedCoords.end()) { - auto otherCoord = *otherQubitsIt; + const auto otherCoord = *otherQubitsIt; if (otherCoord == coord) { - otherQubitsIt++; + ++otherQubitsIt; continue; } if (nearbyFreeIt != nearbyFreeCoords.end()) { - totalTime += this->arch.getVectorShuttlingTime( - this->arch.getVector(otherCoord, *nearbyFreeIt)); - totalTime += this->arch.getShuttlingTime(qc::OpType::AodActivate) + - this->arch.getShuttlingTime(qc::OpType::AodDeactivate); - nearbyFreeIt++; + totalTime += this->arch->getVectorShuttlingTime( + this->arch->getVector(otherCoord, *nearbyFreeIt)); + totalTime += this->arch->getShuttlingTime(qc::OpType::AodActivate) + + this->arch->getShuttlingTime(qc::OpType::AodDeactivate); + ++nearbyFreeIt; totalMoves++; } else if (nearbyOccIt != nearbyOccupiedCoords.end()) { - totalTime += 2 * this->arch.getVectorShuttlingTime( - this->arch.getVector(otherCoord, *nearbyOccIt)); + totalTime += 2 * this->arch->getVectorShuttlingTime( + this->arch->getVector(otherCoord, *nearbyOccIt)); totalTime += - 2 * (this->arch.getShuttlingTime(qc::OpType::AodActivate) + - this->arch.getShuttlingTime(qc::OpType::AodDeactivate)); - nearbyOccIt++; + 2 * (this->arch->getShuttlingTime(qc::OpType::AodActivate) + + this->arch->getShuttlingTime(qc::OpType::AodDeactivate)); + ++nearbyOccIt; totalMoves += 2; } else { throw std::runtime_error("No space to " @@ -1255,7 +1601,7 @@ NeutralAtomMapper::estimateNumMove(const qc::Operation* opPointer) { std::to_string(usedQubits.size())); } - otherQubitsIt++; + ++otherQubitsIt; } if (totalTime < minTime) { @@ -1273,21 +1619,229 @@ bool NeutralAtomMapper::swapGateBetter(const qc::Operation* opPointer) { return true; } auto [minMoves, minTimeMoves] = estimateNumMove(opPointer); - auto fidSwaps = - std::exp(-minTimeSwaps * this->arch.getNqubits() / - this->arch.getDecoherenceTime()) * - std::pow(this->arch.getGateAverageFidelity("swap"), minNumSwaps); - auto fidMoves = - std::exp(-minTimeMoves * this->arch.getNqubits() / - this->arch.getDecoherenceTime()) * + const auto fidSwaps = + std::exp(-minTimeSwaps * this->arch->getNqubits() / + this->arch->getDecoherenceTime()) * + std::pow(this->arch->getGateAverageFidelity("swap"), minNumSwaps); + const auto fidMoves = + std::exp(-minTimeMoves * this->arch->getNqubits() / + this->arch->getDecoherenceTime()) * std::pow( - this->arch.getShuttlingAverageFidelity(qc::OpType::AodMove) * - this->arch.getShuttlingAverageFidelity(qc::OpType::AodActivate) * - this->arch.getShuttlingAverageFidelity(qc::OpType::AodDeactivate), + this->arch->getShuttlingAverageFidelity(qc::OpType::AodMove) * + this->arch->getShuttlingAverageFidelity(qc::OpType::AodActivate) * + this->arch->getShuttlingAverageFidelity( + qc::OpType::AodDeactivate), minMoves); return fidSwaps * parameters.gateWeight > fidMoves * parameters.shuttlingWeight; } +size_t NeutralAtomMapper::gateBasedMapping(NeutralAtomLayer& frontLayer, + NeutralAtomLayer& lookaheadLayer, + size_t i) { + // first do all gate based mapping gates + while (!this->frontLayerGate.empty()) { + GateList gatesToExecute; + while (gatesToExecute.empty() && !this->frontLayerGate.empty()) { + ++i; + if (this->parameters.verbose) { + spdlog::info("iteration {}", i); + } + + auto bestSwap = findBestSwap(lastSwap); + MappingMethod bestMethod = SwapMethod; + if (parameters.maxBridgeDistance > 0 && !multiQubitGates) { + auto bestBridge = findBestBridge(bestSwap); + bestMethod = compareSwapAndBridge(bestSwap, bestBridge); + if (bestMethod == BridgeMethod) { + updateBlockedQubits( + HwQubits(bestBridge.second.begin(), bestBridge.second.end())); + applyBridge(frontLayer, bestBridge); + } + } + if (bestMethod == SwapMethod) { + if (bestSwap == Swap() || bestSwap.first == bestSwap.second) { + throw std::runtime_error( + "No possible SWAP found to execute gates in front layer."); + } + lastSwap = bestSwap; + updateBlockedQubits(HwQubits{bestSwap.first, bestSwap.second}); + applySwap(bestSwap); + } + + gatesToExecute = getExecutableGates(frontLayer.getGates()); + } + mapAllPossibleGates(frontLayer, lookaheadLayer); + reassignGatesToLayers(frontLayer.getGates(), lookaheadLayer.getGates()); + if (this->parameters.verbose) { + printLayers(); + } + } + return i; +} + +MappingMethod +NeutralAtomMapper::compareSwapAndBridge(const Swap& bestSwap, + const Bridge& bestBridge) { + if (bestBridge == Bridge()) { + return SwapMethod; + } + if (this->parameters.dynamicMappingWeight == 0) { + return BridgeMethod; + } + const auto swapFrontDistReduction = + swapDistanceReduction(bestSwap, this->frontLayerGate); + const auto swapLookaheadDistReduction = + swapDistanceReduction(bestSwap, this->lookaheadLayerGate); + const auto swapDistReduction = + swapFrontDistReduction + + (this->parameters.lookaheadWeightSwaps * swapLookaheadDistReduction / + this->parameters.lookaheadDepth); + + // bridge distance reduction + qc::fp const bridgeDistReduction = + static_cast(bestBridge.second.size()) - 2; + + // fidelity comparison + qc::fp const swapTime = + this->arch->getGateTime("swap") + this->arch->getGateTime("cz"); + qc::fp const swapFidelity = + this->arch->getGateAverageFidelity("swap") * + this->arch->getGateAverageFidelity("cz") * + std::exp(-swapTime / this->arch->getDecoherenceTime()); + const std::string bridgeName = + "bridge" + std::to_string(bestBridge.second.size()); + qc::fp const bridgeFidelity = this->arch->getGateAverageFidelity(bridgeName) * + std::exp(-this->arch->getGateTime(bridgeName) / + this->arch->getDecoherenceTime()); + const auto swap = std::log(swapFidelity) / swapDistReduction / + parameters.dynamicMappingWeight; + const auto bridge = std::log(bridgeFidelity) / bridgeDistReduction; + if (swap >= bridge) { + return SwapMethod; + } + return BridgeMethod; +} + +MappingMethod NeutralAtomMapper::compareShuttlingAndFlyingAncilla( + const MoveComb& bestMoveComb, const FlyingAncillaComb& bestFaComb, + const PassByComb& bestPbComb) const { + if (flyingAncillas.getNumQubits() == 0 && !parameters.usePassBy) { + return MoveMethod; + } + if (multiQubitGates) { + return MoveMethod; + } + + // move distance reduction + const auto moveDistReductionFront = + moveCombDistanceReduction(bestMoveComb, this->frontLayerShuttling); + + auto moveDistReductionLookAhead = 0.0; + if (!this->lookaheadLayerShuttling.empty()) { + moveDistReductionLookAhead = + this->parameters.lookaheadWeightMoves * + moveCombDistanceReduction(bestMoveComb, this->lookaheadLayerShuttling) / + static_cast(this->lookaheadLayerShuttling.size()); + } + auto moveDistReduction = moveDistReductionFront + moveDistReductionLookAhead; + // move + auto const moveDist = this->arch->getMoveCombEuclideanDistance(bestMoveComb); + auto const moveCombSize = bestMoveComb.size(); + auto const moveOpFidelity = std::pow( + this->arch->getShuttlingAverageFidelity(qc::OpType::AodMove) * + this->arch->getShuttlingAverageFidelity(qc::OpType::AodActivate) * + this->arch->getShuttlingAverageFidelity(qc::OpType::AodDeactivate), + moveCombSize); + auto const moveTime = + (moveDist / this->arch->getShuttlingTime(qc::OpType::AodMove)) + + (this->arch->getShuttlingTime(qc::OpType::AodActivate) * + static_cast(moveCombSize)) + + (this->arch->getShuttlingTime(qc::OpType::AodDeactivate) * + static_cast(moveCombSize)); + auto const moveDecoherence = std::exp(-moveTime * this->arch->getNqubits() / + this->arch->getDecoherenceTime()); + auto const moveFidelity = moveOpFidelity * moveDecoherence; + + // fa + auto faDistReduction = 0.0; + auto faFidelity = 0.0; + if (flyingAncillas.getNumQubits() != 0) { + // flying ancilla distance reduction + auto const faCoords = this->hardwareQubits.getCoordIndices( + this->mapping.getHwQubits(bestFaComb.op->getUsedQubits())); + faDistReduction = this->arch->getAllToAllEuclideanDistance(faCoords); + + // flying ancilla + auto const faDist = this->arch->getFaEuclideanDistance(bestFaComb); + auto const faCombSize = bestFaComb.moves.size(); + auto const faOpFidelity = std::pow( + std::pow(this->arch->getShuttlingAverageFidelity(qc::OpType::AodMove), + 3) * + std::pow(this->arch->getGateAverageFidelity("cz"), 2) * + std::pow(this->arch->getGateAverageFidelity("h"), 4), + faCombSize); + auto const faDecoherence = std::exp( + -faDist / this->arch->getShuttlingTime(qc::OpType::AodMove) * + this->arch->getNqubits() * 2 / this->arch->getDecoherenceTime()); + faFidelity = faOpFidelity * faDecoherence; + } + + // passby + auto pbDistReduction = 0.0; + auto passByFidelity = 0.0; + if (parameters.usePassBy) { + auto const pbCoords = this->hardwareQubits.getCoordIndices( + this->mapping.getHwQubits(bestPbComb.op->getUsedQubits())); + pbDistReduction = this->arch->getAllToAllEuclideanDistance(pbCoords); + const auto pbCombSize = bestPbComb.moves.size(); + + auto const passByDist = this->arch->getPassByEuclideanDistance(bestPbComb); + auto const passByTime = + (passByDist / this->arch->getShuttlingTime(qc::OpType::AodMove)) * 2 + + (this->arch->getShuttlingTime(qc::OpType::AodActivate) * + static_cast(pbCombSize)) + + (this->arch->getShuttlingTime(qc::OpType::AodDeactivate) * + static_cast(pbCombSize)); + passByFidelity = std::pow(std::pow(this->arch->getShuttlingAverageFidelity( + qc::OpType::AodMove), + 2) * + this->arch->getShuttlingAverageFidelity( + qc::OpType::AodActivate) * + this->arch->getShuttlingAverageFidelity( + qc::OpType::AodDeactivate), + pbCombSize) * + std::exp(-passByTime * this->arch->getNqubits() / + this->arch->getDecoherenceTime()); + } + + auto const minDistanceReduction = + std::min({moveDistReduction, faDistReduction, pbDistReduction}); + constexpr qc::fp constant = 1; + if (minDistanceReduction < 0) { + moveDistReduction -= minDistanceReduction - constant; + faDistReduction -= minDistanceReduction - constant; + pbDistReduction -= minDistanceReduction - constant; + } else { + moveDistReduction += constant; + faDistReduction += constant; + pbDistReduction += constant; + } + + // higher is better + const auto move = std::log(moveFidelity) / moveDistReduction / + parameters.dynamicMappingWeight; + + const auto fa = std::log(faFidelity) / faDistReduction; + const auto passBy = std::log(passByFidelity) / pbDistReduction; + + if (move > fa && move > passBy) { + return MoveMethod; + } + if (fa > move && fa > passBy) { + return FlyingAncillaMethod; + } + return PassByMethod; +} } // namespace na diff --git a/src/hybridmap/HybridSynthesisMapper.cpp b/src/hybridmap/HybridSynthesisMapper.cpp new file mode 100644 index 000000000..941196877 --- /dev/null +++ b/src/hybridmap/HybridSynthesisMapper.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +// +// This file is part of the MQT QMAP library released under the MIT license. +// See README.md or go to https://github.com/cda-tum/qmap for more information. +// +#include "hybridmap/HybridSynthesisMapper.hpp" + +#include "hybridmap/HybridNeutralAtomMapper.hpp" +#include "hybridmap/NeutralAtomDefinitions.hpp" +#include "ir/Definitions.hpp" +#include "ir/QuantumComputation.hpp" + +#include +#include +#include +#include +#include +#include + +namespace na { + +std::vector +HybridSynthesisMapper::evaluateSynthesisSteps(qcs& synthesisSteps, + const bool alsoMap) { + if (synthesisSteps.empty()) { + return {}; + } + if (!initialized) { + initMapping(synthesisSteps[0].getNqubits()); + initialized = true; + } + std::vector> candidates; + size_t qcIndex = 0; + for (auto& qc : synthesisSteps) { + if (parameters.verbose) { + spdlog::info("Evaluating synthesis step number {}", qcIndex); + } + const auto fidelity = evaluateSynthesisStep(qc); + candidates.emplace_back(qc, fidelity); + if (parameters.verbose) { + spdlog::info("Fidelity: {}", fidelity); + } + ++qcIndex; + } + std::vector fidelities; + size_t bestIndex = 0; + qc::fp bestFidelity = 0; + for (size_t i = 0; i < candidates.size(); ++i) { + fidelities.push_back(candidates[i].second); + if (candidates[i].second > bestFidelity) { + bestFidelity = candidates[i].second; + bestIndex = i; + } + } + if (alsoMap && !candidates.empty()) { + appendWithMapping(candidates[bestIndex].first); + } + return fidelities; +} + +qc::fp +HybridSynthesisMapper::evaluateSynthesisStep(qc::QuantumComputation& qc) const { + NeutralAtomMapper tempMapper(arch, parameters); + tempMapper.copyStateFrom(*this); + // Make a copy of qc to avoid modifying the original + auto qcCopy = qc; + auto mappedQc = tempMapper.map(qcCopy, mapping); + tempMapper.convertToAod(); + const auto results = tempMapper.schedule(); + return results.totalFidelities; +} + +void HybridSynthesisMapper::appendWithoutMapping( + const qc::QuantumComputation& qc) { + if (mappedQc.empty()) { + initMapping(qc.getNqubits()); + } + for (const auto& op : qc) { + synthesizedQc.emplace_back(op->clone()); + mapGate(op.get()); + } +} + +void HybridSynthesisMapper::appendWithMapping(qc::QuantumComputation& qc) { + if (mappedQc.empty()) { + initMapping(qc.getNqubits()); + } + mapAppend(qc, mapping); + for (const auto& op : qc) { + synthesizedQc.emplace_back(op->clone()); + } +} + +AdjacencyMatrix HybridSynthesisMapper::getCircuitAdjacencyMatrix() const { + if (!initialized) { + throw std::runtime_error( + "Not yet initialized. Cannot get circuit adjacency matrix."); + } + const auto numCircQubits = synthesizedQc.getNqubits(); + AdjacencyMatrix adjMatrix(numCircQubits); + + for (uint32_t i = 0; i < numCircQubits; ++i) { + for (uint32_t j = 0; j < numCircQubits; ++j) { + if (i == j) { + adjMatrix(i, j) = 0; + continue; + } + const auto mappedI = mapping.getHwQubit(i); + const auto mappedJ = mapping.getHwQubit(j); + if (arch->getSwapDistance(mappedI, mappedJ) == 0) { + adjMatrix(i, j) = 1; + adjMatrix(j, i) = 1; + } else { + adjMatrix(i, j) = 0; + adjMatrix(j, i) = 0; + } + } + } + return adjMatrix; +} + +} // namespace na diff --git a/src/hybridmap/Mapping.cpp b/src/hybridmap/Mapping.cpp index a33175e3c..9271e8c5b 100644 --- a/src/hybridmap/Mapping.cpp +++ b/src/hybridmap/Mapping.cpp @@ -11,25 +11,180 @@ #include "hybridmap/Mapping.hpp" #include "hybridmap/NeutralAtomDefinitions.hpp" +#include "ir/Definitions.hpp" +#include +#include +#include +#include +#include #include +#include +#include +#include namespace na { -void Mapping::applySwap(Swap swap) { - auto q1 = swap.first; - auto q2 = swap.second; - if (this->isMapped(q1) && this->isMapped(q2)) { - auto circQ1 = this->getCircQubit(q1); - auto circQ2 = this->getCircQubit(q2); - this->setCircuitQubit(circQ2, q1); - this->setCircuitQubit(circQ1, q2); - } else if (this->isMapped(q1) && !this->isMapped(q2)) { - this->setCircuitQubit(this->getCircQubit(q1), q2); - } else if (this->isMapped(q2) && !this->isMapped(q1)) { - this->setCircuitQubit(this->getCircQubit(q2), q1); +void Mapping::applySwap(const Swap& swap) { + const auto q1 = swap.first; + const auto q2 = swap.second; + if (isMapped(q1) && isMapped(q2)) { + const auto circQ1 = getCircQubit(q1); + const auto circQ2 = getCircQubit(q2); + setCircuitQubit(circQ2, q1); + setCircuitQubit(circQ1, q2); + } else if (isMapped(q1) && !isMapped(q2)) { + setCircuitQubit(getCircQubit(q1), q2); + } else if (isMapped(q2) && !isMapped(q1)) { + setCircuitQubit(getCircQubit(q2), q1); } else { throw std::runtime_error("Cannot swap unmapped qubits"); } } +std::vector Mapping::graphMatching() { + constexpr auto invalidHw = std::numeric_limits::max(); + constexpr auto invalidCirc = std::numeric_limits::max(); + if (dag.size() > hwQubits.getNumQubits()) { + throw std::runtime_error( + "graphMatching: more circuit qubits than hardware qubits"); + } + std::vector qubitIndices(dag.size(), invalidHw); + std::vector hwIndices(hwQubits.getNumQubits(), invalidCirc); + // make hardware graph + std::unordered_map hwGraph; + for (HwQubit i = 0; i < hwQubits.getNumQubits(); ++i) { + auto neighbors = hwQubits.getNearbyQubits(i); + hwGraph[i] = std::vector(neighbors.begin(), neighbors.end()); + } + for (auto& neighbors : hwGraph | std::views::values) { + std::ranges::sort(neighbors, [this](const HwQubit a, const HwQubit b) { + return hwQubits.getNearbyQubits(a).size() > + hwQubits.getNearbyQubits(b).size(); + }); + } + HwQubit hwCenter = invalidHw; + size_t maxHwConnections = 0; + for (const auto& [qubit, neighbors] : hwGraph) { + if (neighbors.size() > maxHwConnections) { + maxHwConnections = neighbors.size(); + hwCenter = qubit; + } + } + // make circuit graph + std::vector>> circGraph(dag.size()); + for (qc::Qubit qubit = 0; qubit < dag.size(); ++qubit) { + std::unordered_map weightMap; + for (const auto& opPtr : dag[qubit]) { + const auto* op = opPtr->get(); + auto usedQubits = op->getUsedQubits(); + if (usedQubits.size() > 1) { + for (auto i : usedQubits) { + if (i != qubit) { + weightMap[i] += 1.0; + } + } + } + } + std::vector> neighbors(weightMap.begin(), + weightMap.end()); + std::ranges::sort(neighbors, [](const std::pair& a, + const std::pair& b) { + return a.second > b.second; + }); + circGraph[qubit] = std::move(neighbors); + } + // circuit queue for graph matching + std::vector>> nodes; + for (size_t i = 0; i < circGraph.size(); ++i) { + const auto degree = circGraph[i].size(); + double weightSum = 0; + for (const auto& val : circGraph[i] | std::views::values) { + weightSum += val; + } + nodes.emplace_back(i, std::make_pair(degree, weightSum)); + } + std::ranges::sort(nodes.begin(), nodes.end(), + [](const auto& a, const auto& b) { + if (a.second.first == b.second.first) { + return a.second.second > b.second.second; + } + return a.second.first > b.second.first; + }); + std::queue circGraphQueue; + for (const auto& key : nodes | std::views::keys) { + circGraphQueue.push(key); + } + // graph matching -> return qubit Indices + size_t nMapped = 0; + bool firstCenter = true; + while (!circGraphQueue.empty() && nMapped != dag.size()) { + auto qi = circGraphQueue.front(); + HwQubit qI = invalidHw; + // center mapping + if (qubitIndices[qi] == invalidHw) { + // first center + if (firstCenter) { + if (hwCenter == invalidHw) { + throw std::runtime_error("graphMatching: no hardware center qubit"); + } + qI = hwCenter; + firstCenter = false; + } + // next.. + else { + auto minDistance = std::numeric_limits::max(); + for (HwQubit qCandi = 0; qCandi < hwQubits.getNumQubits(); ++qCandi) { + if (hwIndices[qCandi] != invalidCirc) { + continue; + } + auto weightDistance = 0.0; + for (const auto& qnPair : circGraph[qi]) { + auto qn = qnPair.first; + auto qnWeight = qnPair.second; + HwQubit const qN = qubitIndices[qn]; + if (qN == invalidHw) { + continue; + } + weightDistance += + qnWeight * hwQubits.getSwapDistance(qN, qCandi, true); + } + if (weightDistance < minDistance) { + minDistance = weightDistance; + qI = qCandi; + } + } + if (qI == invalidHw) { + throw std::runtime_error( + "graphMatching: no free hardware qubit for circuit qubit"); + } + } + qubitIndices[qi] = qI; + hwIndices[qI] = qi; + nMapped++; + } else { + qI = qubitIndices[qi]; + } + // neighbor mapping + for (auto& key : circGraph[qi] | std::views::keys) { + auto const qn = key; + if (qubitIndices[qn] != invalidHw) { + continue; + } + HwQubit qN = invalidHw; + for (const auto& qCandi : hwGraph[qI]) { + if (hwIndices[qCandi] == invalidCirc) { + qN = qCandi; + break; + } + } + if (qN != invalidHw) { + qubitIndices[qn] = qN; + hwIndices[qN] = qn; + nMapped++; + } + } + circGraphQueue.pop(); + } + return qubitIndices; +} } // namespace na diff --git a/src/hybridmap/MoveToAodConverter.cpp b/src/hybridmap/MoveToAodConverter.cpp index 000eed614..ed6dfc517 100644 --- a/src/hybridmap/MoveToAodConverter.cpp +++ b/src/hybridmap/MoveToAodConverter.cpp @@ -20,12 +20,13 @@ #include "na/entities/Location.hpp" #include +#include #include #include #include #include +#include #include -#include #include #include @@ -33,6 +34,7 @@ namespace na { qc::QuantumComputation MoveToAodConverter::schedule(qc::QuantumComputation& qc) { + initFlyingAncillas(); initMoveGroups(qc); if (moveGroups.empty()) { return qc; @@ -54,7 +56,7 @@ MoveToAodConverter::schedule(qc::QuantumComputation& qc) { for (auto& aodOp : groupIt->processedOpsFinal) { qcScheduled.emplace_back(std::make_unique(aodOp)); } - groupIt++; + ++groupIt; } else if (op->getType() != qc::OpType::Move) { qcScheduled.emplace_back(op->clone()); } @@ -64,21 +66,71 @@ MoveToAodConverter::schedule(qc::QuantumComputation& qc) { return qcScheduled; } +AtomMove MoveToAodConverter::convertOpToMove(qc::Operation* get) const { + auto q1 = get->getTargets().front(); + auto q2 = get->getTargets().back(); + const auto load1 = q1 < arch.getNpositions(); + const auto load2 = q2 < arch.getNpositions(); + while (q1 >= arch.getNpositions()) { + q1 -= arch.getNpositions(); + } + while (q2 >= arch.getNpositions()) { + q2 -= arch.getNpositions(); + } + return {.c1 = q1, .c2 = q2, .load1 = load1, .load2 = load2}; +} +void MoveToAodConverter::initFlyingAncillas() { + if (ancillas.empty()) { + return; + } + std::vector coords; + std::vector dirs; + std::vector starts; + std::vector ends; + std::set rowsActivated; + std::set columnsActivated; + for (const auto& ancilla : ancillas) { + auto coord = ancilla.coord.x + ancilla.coord.y * arch.getNcolumns(); + const auto offsets = ancilla.offset; + coords.emplace_back(coord); + coord -= 2 * arch.getNpositions(); + const auto column = (coord % arch.getNcolumns()); + const auto row = (coord / arch.getNcolumns()); + + const auto offset = + arch.getInterQubitDistance() / arch.getNAodIntermediateLevels(); + columnsActivated.insert(column); + const auto x = column * arch.getInterQubitDistance() + (offsets.x * offset); + dirs.emplace_back(Dimension::X); + starts.emplace_back(x); + ends.emplace_back(x); + rowsActivated.insert(row); + const auto y = row * arch.getInterQubitDistance() + (offsets.y * offset); + dirs.emplace_back(Dimension::Y); + starts.emplace_back(y); + ends.emplace_back(y); + } + const AodOperation aodInit(qc::OpType::AodActivate, coords, dirs, starts, + ends); + qcScheduled.emplace_back(std::make_unique(aodInit)); +} + void MoveToAodConverter::initMoveGroups(qc::QuantumComputation& qc) { MoveGroup currentMoveGroup; MoveGroup const lastMoveGroup; uint32_t idx = 0; for (auto& op : qc) { if (op->getType() == qc::OpType::Move) { - AtomMove const move{op->getTargets()[0], op->getTargets()[1]}; - if (currentMoveGroup.canAdd(move, arch)) { - currentMoveGroup.add(move, idx); + const auto move = convertOpToMove(op.get()); + if (currentMoveGroup.canAddMove(move, arch)) { + currentMoveGroup.addMove(move, idx); } else { moveGroups.emplace_back(currentMoveGroup); currentMoveGroup = MoveGroup(); - currentMoveGroup.add(move, idx); + currentMoveGroup.addMove(move, idx); } - } else if (op->getNqubits() > 1 && !currentMoveGroup.moves.empty()) { + } else if (!currentMoveGroup.moves.empty() || + !currentMoveGroup.movesFa.empty()) { for (const auto& qubit : op->getUsedQubits()) { if (std::ranges::find(currentMoveGroup.qubitsUsedByGates, qubit) == currentMoveGroup.qubitsUsedByGates.end()) { @@ -88,26 +140,36 @@ void MoveToAodConverter::initMoveGroups(qc::QuantumComputation& qc) { } idx++; } - if (!currentMoveGroup.moves.empty()) { + if (!currentMoveGroup.moves.empty() || !currentMoveGroup.movesFa.empty()) { moveGroups.emplace_back(std::move(currentMoveGroup)); } } -bool MoveToAodConverter::MoveGroup::canAdd( +bool MoveToAodConverter::MoveGroup::canAddMove( const AtomMove& move, const NeutralAtomArchitecture& archArg) { // if move would move a qubit that is used by a gate in this move group // return false - if (std::ranges::find(qubitsUsedByGates, move.first) != + if (std::ranges::find(qubitsUsedByGates, move.c1) != qubitsUsedByGates.end()) { return false; } // checks if the op can be executed in parallel - auto moveVector = archArg.getVector(move.first, move.second); + const auto& movesToCheck = (move.load1 || move.load2) ? moves : movesFa; return std::ranges::all_of( - moves, - [&moveVector, &archArg](const std::pair opPair) { - auto moveGroup = opPair.first; - auto opVector = archArg.getVector(moveGroup.first, moveGroup.second); + movesToCheck, + [&move, &archArg](const std::pair& opPair) { + const auto& moveGroup = opPair.first; + // check that passby and move are not in same group + if (move.load1 != moveGroup.load1 || move.load2 != moveGroup.load2) { + return false; + } + // if start or end is same -> false + if (move.c1 == moveGroup.c1 || move.c2 == moveGroup.c2) { + return false; + } + // check if parallel executable + const auto moveVector = archArg.getVector(move.c1, move.c2); + const auto opVector = archArg.getVector(moveGroup.c1, moveGroup.c2); return parallelCheck(moveVector, opVector); }); } @@ -128,15 +190,20 @@ bool MoveToAodConverter::MoveGroup::parallelCheck(const MoveVector& v1, return true; } -void MoveToAodConverter::MoveGroup::add(const AtomMove& move, - const uint32_t idx) { - moves.emplace_back(move, idx); - qubitsUsedByGates.emplace_back(move.second); +void MoveToAodConverter::MoveGroup::addMove(const AtomMove& move, + const uint32_t idx) { + if (move.load1 || move.load2) { + moves.emplace_back(move, idx); + } else { + movesFa.emplace_back(move, idx); + } + qubitsUsedByGates.emplace_back(move.c2); } void MoveToAodConverter::AodActivationHelper::addActivation( - std::pair merge, - const Location& origin, const AtomMove& move, MoveVector v) { + const std::pair& merge, + const Location& origin, const AtomMove& move, const MoveVector& v, + bool needLoad) { const auto x = static_cast(origin.x); const auto y = static_cast(origin.y); const auto signX = v.direction.getSignX(); @@ -150,19 +217,20 @@ void MoveToAodConverter::AodActivationHelper::addActivation( case ActivationMergeType::Trivial: switch (merge.second) { case ActivationMergeType::Trivial: - allActivations.emplace_back( - AodActivation{{x, deltaX, signX}, {y, deltaY, signY}, move}); + allActivations.emplace_back(AodActivation{ + {x, deltaX, signX, needLoad}, {y, deltaY, signY, needLoad}, move}); break; case ActivationMergeType::Merge: - mergeActivationDim(Dimension::Y, - AodActivation{Dimension::Y, {y, deltaY, signY}, move}, - AodActivation{Dimension::X, {x, deltaX, signX}, move}); + mergeActivationDim( + Dimension::Y, + AodActivation{Dimension::Y, {y, deltaY, signY, needLoad}, move}, + AodActivation{Dimension::X, {x, deltaX, signX, needLoad}, move}); aodMovesY = getAodMovesFromInit(Dimension::Y, y); reAssignOffsets(aodMovesY, signY); break; case ActivationMergeType::Append: - allActivations.emplace_back( - AodActivation{{x, deltaX, signX}, {y, deltaY, signY}, move}); + allActivations.emplace_back(AodActivation{ + {x, deltaX, signX, needLoad}, {y, deltaY, signY, needLoad}, move}); aodMovesY = getAodMovesFromInit(Dimension::Y, y); reAssignOffsets(aodMovesY, signY); break; @@ -173,18 +241,24 @@ void MoveToAodConverter::AodActivationHelper::addActivation( case ActivationMergeType::Merge: switch (merge.second) { case ActivationMergeType::Trivial: - mergeActivationDim(Dimension::X, - AodActivation{Dimension::X, {x, deltaX, signX}, move}, - AodActivation{Dimension::Y, {y, deltaY, signY}, move}); + mergeActivationDim( + Dimension::X, + AodActivation{Dimension::X, {x, deltaX, signX, needLoad}, move}, + AodActivation{Dimension::Y, {y, deltaY, signY, needLoad}, move}); aodMovesX = getAodMovesFromInit(Dimension::X, x); reAssignOffsets(aodMovesX, signX); break; case ActivationMergeType::Merge: - throw std::runtime_error("Merge in both dimensions should never happen."); + mergeActivationDim( + Dimension::X, + AodActivation{Dimension::X, {x, deltaX, signX, needLoad}, move}, + AodActivation{Dimension::Y, {y, deltaY, signY, needLoad}, move}); + break; case ActivationMergeType::Append: - mergeActivationDim(Dimension::X, - AodActivation{Dimension::X, {x, deltaX, signX}, move}, - AodActivation{Dimension::Y, {y, deltaY, signY}, move}); + mergeActivationDim( + Dimension::X, + AodActivation{Dimension::X, {x, deltaX, signX, needLoad}, move}, + AodActivation{Dimension::Y, {y, deltaY, signY, needLoad}, move}); aodMovesY = getAodMovesFromInit(Dimension::Y, y); reAssignOffsets(aodMovesY, signY); break; @@ -195,21 +269,22 @@ void MoveToAodConverter::AodActivationHelper::addActivation( case ActivationMergeType::Append: switch (merge.second) { case ActivationMergeType::Trivial: - allActivations.emplace_back( - AodActivation{{x, deltaX, signX}, {y, deltaY, signY}, move}); + allActivations.emplace_back(AodActivation{ + {x, deltaX, signX, needLoad}, {y, deltaY, signY, needLoad}, move}); aodMovesX = getAodMovesFromInit(Dimension::X, x); reAssignOffsets(aodMovesX, signX); break; case ActivationMergeType::Merge: - mergeActivationDim(Dimension::Y, - AodActivation{Dimension::Y, {y, deltaY, signY}, move}, - AodActivation{Dimension::X, {x, deltaX, signX}, move}); + mergeActivationDim( + Dimension::Y, + AodActivation{Dimension::Y, {y, deltaY, signY, needLoad}, move}, + AodActivation{Dimension::X, {x, deltaX, signX, needLoad}, move}); aodMovesX = getAodMovesFromInit(Dimension::X, x); reAssignOffsets(aodMovesX, signX); break; case ActivationMergeType::Append: - allActivations.emplace_back( - AodActivation{{x, deltaX, signX}, {y, deltaY, signY}, move}); + allActivations.emplace_back(AodActivation{ + {x, deltaX, signX, needLoad}, {y, deltaY, signY, needLoad}, move}); aodMovesX = getAodMovesFromInit(Dimension::X, x); reAssignOffsets(aodMovesX, signX); aodMovesY = getAodMovesFromInit(Dimension::Y, y); @@ -223,21 +298,37 @@ void MoveToAodConverter::AodActivationHelper::addActivation( break; } } +void MoveToAodConverter::AodActivationHelper::addActivationFa( + const Location& origin, const AtomMove& move, const MoveVector& v, + bool needLoad) { + const auto x = static_cast(origin.x); + const auto y = static_cast(origin.y); + const auto signX = v.direction.getSignX(); + const auto signY = v.direction.getSignY(); + const auto deltaX = v.xEnd - v.xStart; + const auto deltaY = v.yEnd - v.yStart; + + allActivations.emplace_back(AodActivation{ + {x, deltaX, signX, needLoad}, {y, deltaY, signY, needLoad}, move}); +} [[nodiscard]] std::pair MoveToAodConverter::canAddActivation( const AodActivationHelper& activationHelper, const AodActivationHelper& deactivationHelper, const Location& origin, const MoveVector& v, const Location& final, const MoveVector& vReverse, - Dimension dim) { - auto start = + const Dimension dim) { + const auto start = static_cast(dim == Dimension::X ? origin.x : origin.y); - auto end = + const auto end = static_cast(dim == Dimension::X ? final.x : final.y); + const auto delta = static_cast(end - start); // Get Moves that start/end at the same position as the current move - auto aodMovesActivation = activationHelper.getAodMovesFromInit(dim, start); - auto aodMovesDeactivation = deactivationHelper.getAodMovesFromInit(dim, end); + const auto aodMovesActivation = + activationHelper.getAodMovesFromInit(dim, start); + const auto aodMovesDeactivation = + deactivationHelper.getAodMovesFromInit(dim, end); // both empty if (aodMovesActivation.empty() && aodMovesDeactivation.empty()) { @@ -268,7 +359,9 @@ MoveToAodConverter::canAddActivation( for (const auto& aodMoveActivation : aodMovesActivation) { for (const auto& aodMoveDeactivation : aodMovesDeactivation) { if (aodMoveActivation->init == start && - aodMoveDeactivation->init == end) { + aodMoveDeactivation->init == end && + std::abs(aodMoveActivation->delta) == std::abs(delta) && + std::abs(aodMoveDeactivation->delta) == std::abs(delta)) { return std::make_pair(ActivationMergeType::Merge, ActivationMergeType::Merge); } @@ -286,13 +379,13 @@ MoveToAodConverter::canAddActivation( } void MoveToAodConverter::AodActivationHelper::reAssignOffsets( - std::vector>& aodMoves, int32_t sign) { + std::vector>& aodMoves, const int32_t sign) { std::ranges::sort(aodMoves, [](const std::shared_ptr& a, const std::shared_ptr& b) { return std::abs(a->delta) < std::abs(b->delta); }); int32_t offset = sign; - for (auto& aodMove : aodMoves) { + for (const auto& aodMove : aodMoves) { // same sign if (aodMove->delta * sign >= 0) { aodMove->offset = offset; @@ -305,124 +398,179 @@ void MoveToAodConverter::processMoveGroups() { // convert the moves from MoveGroup to AodOperations for (auto groupIt = moveGroups.begin(); groupIt != moveGroups.end(); ++groupIt) { - AodActivationHelper aodActivationHelper{arch, qc::OpType::AodActivate}; - AodActivationHelper aodDeactivationHelper{arch, qc::OpType::AodDeactivate}; - MoveGroup possibleNewMoveGroup; - std::vector movesToRemove; - for (auto& movePair : groupIt->moves) { - auto& move = movePair.first; - auto idx = movePair.second; - auto origin = arch.getCoordinate(move.first); - auto target = arch.getCoordinate(move.second); - auto v = arch.getVector(move.first, move.second); - auto vReverse = arch.getVector(move.second, move.first); - auto canAddX = - canAddActivation(aodActivationHelper, aodDeactivationHelper, origin, - v, target, vReverse, Dimension::X); - auto canAddY = - canAddActivation(aodActivationHelper, aodDeactivationHelper, origin, - v, target, vReverse, Dimension::Y); - auto activationCanAddXY = std::make_pair(canAddX.first, canAddY.first); - auto deactivationCanAddXY = - std::make_pair(canAddX.second, canAddY.second); - if (activationCanAddXY.first == ActivationMergeType::Impossible || - activationCanAddXY.second == ActivationMergeType::Impossible || - deactivationCanAddXY.first == ActivationMergeType::Impossible || - deactivationCanAddXY.second == ActivationMergeType::Impossible) { - // move could not be added as not sufficient intermediate levels - // add new move group and add move to it - possibleNewMoveGroup.add(move, idx); - movesToRemove.emplace_back(move); - } else { - aodActivationHelper.addActivation(activationCanAddXY, origin, move, v); - aodDeactivationHelper.addActivation(deactivationCanAddXY, target, move, - vReverse); - } - } + AodActivationHelper aodActivationHelper{arch, qc::OpType::AodActivate, + (&ancillas)}; + AodActivationHelper aodDeactivationHelper{arch, qc::OpType::AodDeactivate, + (&ancillas)}; + + const auto resultMoves = processMoves(groupIt->moves, aodActivationHelper, + aodDeactivationHelper); + auto movesToRemove = resultMoves.first; + auto possibleNewMoveGroup = resultMoves.second; + + processMovesFa(groupIt->movesFa, aodActivationHelper, + aodDeactivationHelper); + // remove from current move group for (const auto& moveToRemove : movesToRemove) { groupIt->moves.erase( - std::remove_if(groupIt->moves.begin(), groupIt->moves.end(), - [&moveToRemove](const auto& movePair) { - return movePair.first == moveToRemove; - }), + std::ranges::remove_if(groupIt->moves, + [&moveToRemove](const auto& movePair) { + return movePair.first == moveToRemove; + }) + .begin(), groupIt->moves.end()); } if (!possibleNewMoveGroup.moves.empty()) { groupIt = moveGroups.emplace(groupIt + 1, std::move(possibleNewMoveGroup)); possibleNewMoveGroup = MoveGroup(); - groupIt--; + --groupIt; } groupIt->processedOpsInit = aodActivationHelper.getAodOperations(); groupIt->processedOpsFinal = aodDeactivationHelper.getAodOperations(); groupIt->processedOpShuttle = MoveGroup::connectAodOperations( - groupIt->processedOpsInit, groupIt->processedOpsFinal); + aodActivationHelper, aodDeactivationHelper); + } +} + +std::pair, MoveToAodConverter::MoveGroup> +MoveToAodConverter::processMoves( + const std::vector>& moves, + AodActivationHelper& aodActivationHelper, + AodActivationHelper& aodDeactivationHelper) const { + + MoveGroup possibleNewMoveGroup; + std::vector movesToRemove; + for (const auto& movePair : moves) { + const auto& move = movePair.first; + const auto idx = movePair.second; + auto origin = arch.getCoordinate(move.c1); + auto target = arch.getCoordinate(move.c2); + auto v = arch.getVector(move.c1, move.c2); + auto vReverse = arch.getVector(move.c2, move.c1); + auto canAddX = canAddActivation(aodActivationHelper, aodDeactivationHelper, + origin, v, target, vReverse, Dimension::X); + auto canAddY = canAddActivation(aodActivationHelper, aodDeactivationHelper, + origin, v, target, vReverse, Dimension::Y); + const auto activationCanAddXY = + std::make_pair(canAddX.first, canAddY.first); + const auto deactivationCanAddXY = + std::make_pair(canAddX.second, canAddY.second); + if (activationCanAddXY.first == ActivationMergeType::Impossible || + activationCanAddXY.second == ActivationMergeType::Impossible || + deactivationCanAddXY.first == ActivationMergeType::Impossible || + deactivationCanAddXY.second == ActivationMergeType::Impossible) { + // move could not be added as not sufficient intermediate levels + // add new move group and add move to it + possibleNewMoveGroup.addMove(move, idx); + movesToRemove.emplace_back(move); + } else { + aodActivationHelper.addActivation(activationCanAddXY, origin, move, v, + move.load1); + aodDeactivationHelper.addActivation(deactivationCanAddXY, target, move, + vReverse, move.load2); + } + } + + return {movesToRemove, possibleNewMoveGroup}; +} +void MoveToAodConverter::processMovesFa( + const std::vector>& movesFa, + AodActivationHelper& aodActivationHelper, + AodActivationHelper& aodDeactivationHelper) const { + for (const auto& key : movesFa | std::views::keys) { + const auto& moveFa = key; + auto origin = arch.getCoordinate(moveFa.c1); + auto target = arch.getCoordinate(moveFa.c2); + const auto v = arch.getVector(moveFa.c1, moveFa.c2); + const auto vReverse = arch.getVector(moveFa.c2, moveFa.c1); + + aodActivationHelper.addActivationFa(origin, moveFa, v, moveFa.load1); + aodDeactivationHelper.addActivationFa(target, moveFa, vReverse, + moveFa.load2); } } AodOperation MoveToAodConverter::MoveGroup::connectAodOperations( - const std::vector& opsInit, - const std::vector& opsFinal) { + const AodActivationHelper& aodActivationHelper, + const AodActivationHelper& aodDeactivationHelper) { // for each init operation find the corresponding final operation // and connect with an aod move operations // all can be done in parallel in a single move std::vector aodOperations; - std::set targetQubits; - - for (const auto& opInit : opsInit) { - if (opInit.getType() == qc::OpType::AodMove) { - for (const auto& opFinal : opsFinal) { - if (opFinal.getType() == qc::OpType::AodMove) { - if (opInit.getTargets().size() <= 1 || - opFinal.getTargets().size() <= 1) { - throw std::runtime_error( - "AodScheduler::MoveGroup::connectAodOperations: " - "AodMove operation with less than 2 targets"); + std::vector targetQubits; + + const auto d = aodActivationHelper.arch->getInterQubitDistance(); + const auto interD = aodActivationHelper.arch->getInterQubitDistance() / + aodActivationHelper.arch->getNAodIntermediateLevels(); + + constexpr std::array dimensions{Dimension::X, Dimension::Y}; + + // connect move operations + for (const auto& activation : aodActivationHelper.allActivations) { + for (const auto& deactivation : aodDeactivationHelper.allActivations) { + if (activation.moves == deactivation.moves) { + // get target qubits + qc::Targets starts; + qc::Targets ends; + const auto nPos = aodActivationHelper.arch->getNpositions(); + for (const auto& move : activation.moves) { + if (move.load1) { + starts.emplace_back(move.c1); + } else if (move.load2) { + starts.emplace_back(move.c1 + nPos); + } else { + starts.emplace_back(move.c1 + (2 * nPos)); } - if (opInit.getTargets() == opFinal.getTargets()) { - targetQubits.insert(opInit.getTargets().begin(), - opInit.getTargets().end()); - // found corresponding final operation - // connect with aod move - const auto startXs = opInit.getEnds(Dimension::X); - const auto endXs = opFinal.getStarts(Dimension::X); - const auto startYs = opInit.getEnds(Dimension::Y); - const auto endYs = opFinal.getStarts(Dimension::Y); - if (!startXs.empty() && !endXs.empty()) { - for (size_t i = 0; i < startXs.size(); i++) { - const auto startX = startXs[i]; - const auto endX = endXs[i]; - if (std::abs(startX - endX) > 0.0001) { - aodOperations.emplace_back(Dimension::X, startX, endX); - } - } - } - if (!startYs.empty() && !endYs.empty()) { - for (size_t i = 0; i < startYs.size(); i++) { - const auto startY = startYs[i]; - const auto endY = endYs[i]; - if (std::abs(startY - endY) > 0.0001) { - aodOperations.emplace_back(Dimension::Y, startY, endY); - } - } + if (move.load2) { + ends.emplace_back(move.c2); + } else if (move.load1) { + ends.emplace_back(move.c2 + nPos); + } else { + ends.emplace_back(move.c2 + (2 * nPos)); + } + } + + // Ensure that the ordering of the target qubits such that atoms are + // moved away before used as a target + for (size_t i = 0; i < starts.size(); i++) { + const auto pos = std::ranges::find(targetQubits, starts[i]); + if (pos == targetQubits.end()) { + // if the start qubit is not already in the target qubits + targetQubits.emplace_back(starts[i]); + targetQubits.emplace_back(ends[i]); + } else { + // insert the (end, start) pair immediately before the existing + // start + const auto newPos = targetQubits.insert(pos, ends[i]); + targetQubits.insert(newPos, starts[i]); + } + } + + for (const auto& dim : dimensions) { + const auto& activationDim = activation.getActivates(dim); + const auto& deactivationDim = deactivation.getActivates(dim); + for (size_t i = 0; i < activationDim.size(); i++) { + const auto& start = + activationDim[i]->init * d + activationDim[i]->offset * interD; + const auto& end = deactivationDim[i]->init * d + + deactivationDim[i]->offset * interD; + if (std::abs(start - end) > 0.0001) { + aodOperations.emplace_back(dim, start, end); } } } } } } - std::vector targetQubitsVec; - targetQubitsVec.reserve(targetQubits.size()); - for (const auto& qubit : targetQubits) { - targetQubitsVec.emplace_back(qubit); - } - return {qc::OpType::AodMove, targetQubitsVec, aodOperations}; + + return {qc::OpType::AodMove, targetQubits, aodOperations}; } std::vector> MoveToAodConverter::AodActivationHelper::getAodMovesFromInit( - Dimension dim, uint32_t init) const { + const Dimension dim, const uint32_t init) const { std::vector> aodMoves; for (const auto& activation : allActivations) { for (auto& aodMove : activation.getActivates(dim)) { @@ -435,14 +583,11 @@ MoveToAodConverter::AodActivationHelper::getAodMovesFromInit( } uint32_t MoveToAodConverter::AodActivationHelper::getMaxOffsetAtInit( - Dimension dim, uint32_t init, int32_t sign) const { - auto aodMoves = getAodMovesFromInit(dim, init); - if (aodMoves.empty()) { - return 0; - } + const Dimension dim, const uint32_t init, const int32_t sign) const { + const auto aodMoves = getAodMovesFromInit(dim, init); uint32_t maxOffset = 0; for (const auto& aodMove : aodMoves) { - auto offset = aodMove->offset; + const auto offset = aodMove->offset; if (offset * sign >= 0) { maxOffset = std::max(maxOffset, static_cast(std::abs(offset))); } @@ -451,7 +596,7 @@ uint32_t MoveToAodConverter::AodActivationHelper::getMaxOffsetAtInit( } bool MoveToAodConverter::AodActivationHelper::checkIntermediateSpaceAtInit( - Dimension dim, uint32_t init, int32_t sign) const { + const Dimension dim, const uint32_t init, const int32_t sign) const { uint32_t neighborX = init; if (sign > 0) { neighborX += 1; @@ -459,30 +604,47 @@ bool MoveToAodConverter::AodActivationHelper::checkIntermediateSpaceAtInit( neighborX -= 1; } auto aodMoves = getAodMovesFromInit(dim, init); - auto aodMovesNeighbor = getAodMovesFromInit(dim, neighborX); - if (aodMoves.empty() && aodMovesNeighbor.empty()) { - return true; - } - if (aodMoves.empty()) { - return getMaxOffsetAtInit(dim, neighborX, sign) < - arch.getNAodIntermediateLevels(); - } + const auto aodMovesNeighbor = getAodMovesFromInit(dim, neighborX); if (aodMovesNeighbor.empty()) { return getMaxOffsetAtInit(dim, init, sign) < - arch.getNAodIntermediateLevels(); + arch->getNAodIntermediateLevels(); } return getMaxOffsetAtInit(dim, init, sign) + getMaxOffsetAtInit(dim, neighborX, sign) < - arch.getNAodIntermediateLevels(); + arch->getNAodIntermediateLevels(); +} +void MoveToAodConverter::AodActivationHelper::computeInitAndOffsetOperations( + Dimension dimension, const std::shared_ptr& aodMove, + std::vector& initOperations, + std::vector& offsetOperations) const { + + const auto d = arch->getInterQubitDistance(); + const auto interD = + arch->getInterQubitDistance() / arch->getNAodIntermediateLevels(); + + initOperations.emplace_back(dimension, static_cast(aodMove->init) * d, + static_cast(aodMove->init) * d); + if (type == qc::OpType::AodActivate) { + offsetOperations.emplace_back( + dimension, static_cast(aodMove->init) * d, + static_cast(aodMove->init) * d + + static_cast(aodMove->offset) * interD); + } else { + offsetOperations.emplace_back(dimension, + static_cast(aodMove->init) * d + + static_cast(aodMove->offset) * + interD, + static_cast(aodMove->init) * d); + } } void MoveToAodConverter::AodActivationHelper::mergeActivationDim( - Dimension dim, const AodActivation& activationDim, + const Dimension dim, const AodActivation& activationDim, const AodActivation& activationOtherDim) { // merge activations for (auto& activationCurrent : allActivations) { auto activates = activationCurrent.getActivates(dim); - for (auto& aodMove : activates) { + for (const auto& aodMove : activates) { if (aodMove->init == activationDim.getActivates(dim)[0]->init && aodMove->delta == activationDim.getActivates(dim)[0]->delta) { // append move @@ -501,78 +663,50 @@ void MoveToAodConverter::AodActivationHelper::mergeActivationDim( } } -std::pair +std::vector MoveToAodConverter::AodActivationHelper::getAodOperation( - const AodActivationHelper::AodActivation& activation) const { - std::vector qubitsActivation; + const AodActivation& activation) const { + CoordIndices qubitsActivation; qubitsActivation.reserve(activation.moves.size()); for (const auto& move : activation.moves) { if (type == qc::OpType::AodActivate) { - qubitsActivation.emplace_back(move.first); + if (move.load1) { + qubitsActivation.emplace_back(move.c1); + } } else { - qubitsActivation.emplace_back(move.second); + if (move.load2) { + qubitsActivation.emplace_back(move.c2); + } } } - std::vector qubitsMove; - qubitsMove.reserve(activation.moves.size() * 2); - for (const auto& move : activation.moves) { - if (std::ranges::find(qubitsMove, move.first) == qubitsMove.end()) { - qubitsMove.emplace_back(move.first); - } - if (std::ranges::find(qubitsMove, move.second) == qubitsMove.end()) { - qubitsMove.emplace_back(move.second); - } + CoordIndices qubitsOffset; + qubitsOffset.reserve(activation.moves.size() * 2); + for (const auto& qubit : qubitsActivation) { + qubitsOffset.emplace_back(qubit); + qubitsOffset.emplace_back(qubit); } std::vector initOperations; std::vector offsetOperations; - auto d = this->arch.getInterQubitDistance(); - auto interD = this->arch.getInterQubitDistance() / - this->arch.getNAodIntermediateLevels(); - for (const auto& aodMove : activation.activateXs) { - initOperations.emplace_back(Dimension::X, - static_cast(aodMove->init) * d, - static_cast(aodMove->init) * d); - if (type == qc::OpType::AodActivate) { - offsetOperations.emplace_back( - Dimension::X, static_cast(aodMove->init) * d, - static_cast(aodMove->init) * d + - static_cast(aodMove->offset) * interD); - } else { - offsetOperations.emplace_back(Dimension::X, - static_cast(aodMove->init) * d + - static_cast(aodMove->offset) * - interD, - static_cast(aodMove->init) * d); + if (aodMove->load) { + computeInitAndOffsetOperations(Dimension::X, aodMove, initOperations, + offsetOperations); } } for (const auto& aodMove : activation.activateYs) { - initOperations.emplace_back(Dimension::Y, - static_cast(aodMove->init) * d, - static_cast(aodMove->init) * d); - if (type == qc::OpType::AodActivate) { - offsetOperations.emplace_back( - Dimension::Y, static_cast(aodMove->init) * d, - static_cast(aodMove->init) * d + - static_cast(aodMove->offset) * interD); - } else { - offsetOperations.emplace_back(Dimension::Y, - static_cast(aodMove->init) * d + - static_cast(aodMove->offset) * - interD, - static_cast(aodMove->init) * d); + if (aodMove->load) { + computeInitAndOffsetOperations(Dimension::Y, aodMove, initOperations, + offsetOperations); } } - - auto initOp = AodOperation(type, qubitsActivation, initOperations); - auto offsetOp = - AodOperation(qc::OpType::AodMove, qubitsMove, offsetOperations); - if (this->type == qc::OpType::AodActivate) { - return std::make_pair(initOp, offsetOp); + if (initOperations.empty() && offsetOperations.empty()) { + return {}; } - return std::make_pair(offsetOp, initOp); + + return {AodOperation(type, qubitsActivation, initOperations), + AodOperation(qc::OpType::AodMove, qubitsOffset, offsetOperations)}; } std::vector @@ -580,10 +714,14 @@ MoveToAodConverter::AodActivationHelper::getAodOperations() const { std::vector aodOperations; for (const auto& activation : allActivations) { auto operations = getAodOperation(activation); - aodOperations.emplace_back(std::move(operations.first)); - aodOperations.emplace_back(std::move(operations.second)); + // insert ancilla dodging operations + aodOperations.insert(aodOperations.end(), operations.begin(), + operations.end()); } + if (type == qc::OpType::AodActivate) { + return aodOperations; + } + std::ranges::reverse(aodOperations); return aodOperations; } - } // namespace na diff --git a/src/hybridmap/NeutralAtomArchitecture.cpp b/src/hybridmap/NeutralAtomArchitecture.cpp index 610198d20..7de065171 100644 --- a/src/hybridmap/NeutralAtomArchitecture.cpp +++ b/src/hybridmap/NeutralAtomArchitecture.cpp @@ -12,6 +12,7 @@ #include "datastructures/SymmetricMatrix.hpp" #include "hybridmap/NeutralAtomDefinitions.hpp" +#include "hybridmap/NeutralAtomUtils.hpp" #include "ir/Definitions.hpp" #include "ir/operations/AodOperation.hpp" #include "ir/operations/OpType.hpp" @@ -19,12 +20,15 @@ #include "na/entities/Location.hpp" #include +#include #include #include +#include #include #include #include #include +#include // added #include #include #include @@ -47,7 +51,7 @@ void NeutralAtomArchitecture::loadJson(const std::string& filename) { // Load properties nlohmann::basic_json<> jsonDataProperties = jsonData["properties"]; - this->properties = Properties( + properties = Properties( jsonDataProperties["nRows"], jsonDataProperties["nColumns"], jsonDataProperties["nAods"], jsonDataProperties["nAodCoordinates"], jsonDataProperties["interQubitDistance"], @@ -57,11 +61,11 @@ void NeutralAtomArchitecture::loadJson(const std::string& filename) { // Load parameters const nlohmann::basic_json<> jsonDataParameters = jsonData["parameters"]; - this->parameters = Parameters(); - this->parameters.nQubits = jsonDataParameters["nQubits"]; + parameters = Parameters(); + parameters.nQubits = jsonDataParameters["nQubits"]; // check if qubits can fit in the architecture - if (this->parameters.nQubits > this->properties.getNpositions()) { + if (parameters.nQubits > properties.getNpositions()) { throw std::runtime_error("Number of qubits exceeds number of positions"); } @@ -69,13 +73,30 @@ void NeutralAtomArchitecture::loadJson(const std::string& filename) { for (const auto& [key, value] : jsonDataParameters["gateTimes"].items()) { gateTimes.emplace(key, value); } - this->parameters.gateTimes = gateTimes; + // check if cz and h gates are present (require explicit fallback) + auto ensureGateWithFallback = [](auto& map, const std::string& gate, + const std::string& fallback) { + if (map.contains(gate)) { + return; + } + if (!map.contains(fallback)) { + throw std::runtime_error("Missing gate entry \"" + gate + + "\" and fallback \"" + fallback + "\""); + } + map[gate] = map.at(fallback); + }; + + ensureGateWithFallback(gateTimes, "cz", "none"); + ensureGateWithFallback(gateTimes, "h", "none"); + parameters.gateTimes = gateTimes; std::map gateAverageFidelities; for (const auto& [key, value] : jsonDataParameters["gateAverageFidelities"].items()) { gateAverageFidelities.emplace(key, value); } - this->parameters.gateAverageFidelities = gateAverageFidelities; + ensureGateWithFallback(gateAverageFidelities, "cz", "none"); + ensureGateWithFallback(gateAverageFidelities, "h", "none"); + parameters.gateAverageFidelities = gateAverageFidelities; std::map shuttlingTimes; for (const auto& [key, value] : @@ -83,31 +104,44 @@ void NeutralAtomArchitecture::loadJson(const std::string& filename) { shuttlingTimes.emplace(qc::opTypeFromString(key), value); } // compute values for SWAP gate - qc::fp swapGateTime = 0; - qc::fp swapGateFidelity = 1; - for (size_t i = 0; i < 3; ++i) { - swapGateTime += gateTimes.at("cz"); - swapGateFidelity *= gateAverageFidelities.at("cz"); - } - for (size_t i = 0; i < 6; ++i) { - swapGateTime += gateTimes.at("h"); - swapGateFidelity *= gateAverageFidelities.at("h"); + qc::fp const swapGateTime = + (gateTimes.at("cz") * 3) + (gateTimes.at("h") * 4); + qc::fp const swapGateFidelity = + std::pow(gateAverageFidelities.at("cz"), 3) * + std::pow(gateAverageFidelities.at("h"), 6); + parameters.gateTimes.emplace("swap", swapGateTime); + parameters.gateAverageFidelities.emplace("swap", swapGateFidelity); + + // compute values for Bridge gate + // precompute bridge circuits + const auto maxIdx = + std::min({bridgeCircuits.czDepth.size(), bridgeCircuits.hDepth.size(), + bridgeCircuits.czs.size(), bridgeCircuits.hs.size()}); + for (size_t i = 3; i < std::min(10, maxIdx); ++i) { + qc::fp const bridgeGateTime = + (static_cast(bridgeCircuits.czDepth[i]) * + gateTimes.at("cz")) + + (static_cast(bridgeCircuits.hDepth[i]) * gateTimes.at("h")); + qc::fp const bridgeFidelity = + std::pow(gateAverageFidelities.at("cz"), bridgeCircuits.czs[i]) * + std::pow(gateAverageFidelities.at("h"), bridgeCircuits.hs[i]); + parameters.gateTimes.emplace("bridge" + std::to_string(i), + bridgeGateTime); + parameters.gateAverageFidelities.emplace("bridge" + std::to_string(i), + bridgeFidelity); } - this->parameters.gateTimes.emplace("swap", swapGateTime); - this->parameters.gateAverageFidelities.emplace("swap", swapGateFidelity); - this->parameters.shuttlingTimes = shuttlingTimes; + parameters.shuttlingTimes = shuttlingTimes; std::map shuttlingAverageFidelities; for (const auto& [key, value] : jsonDataParameters["shuttlingAverageFidelities"].items()) { shuttlingAverageFidelities.emplace(qc::opTypeFromString(key), value); } - this->parameters.shuttlingAverageFidelities = shuttlingAverageFidelities; + parameters.shuttlingAverageFidelities = shuttlingAverageFidelities; - this->parameters.decoherenceTimes = - NeutralAtomArchitecture::Parameters::DecoherenceTimes{ - .t1 = jsonDataParameters["decoherenceTimes"]["t1"], - .t2 = jsonDataParameters["decoherenceTimes"]["t2"]}; + parameters.decoherenceTimes = Parameters::DecoherenceTimes{ + .t1 = jsonDataParameters["decoherenceTimes"]["t1"], + .t2 = jsonDataParameters["decoherenceTimes"]["t2"]}; } catch (std::exception& e) { throw std::runtime_error("Could not parse JSON file " + filename + ": " + @@ -115,26 +149,27 @@ void NeutralAtomArchitecture::loadJson(const std::string& filename) { } // apply changes to the object - this->name = jsonData["name"]; + name = jsonData["name"]; - this->createCoordinates(); - this->computeSwapDistances(this->properties.getInteractionRadius()); - this->computeNearbyCoordinates(); + createCoordinates(); + computeSwapDistances(properties.getInteractionRadius()); + computeNearbyCoordinates(); } void NeutralAtomArchitecture::createCoordinates() { coordinates.reserve(properties.getNpositions()); - for (std::uint16_t i = 0; i < this->properties.getNpositions(); i++) { - this->coordinates.emplace_back( - Location{.x = static_cast(i % this->properties.getNcolumns()), + for (std::uint16_t i = 0; i < properties.getNpositions(); i++) { + coordinates.emplace_back( + Location{.x = static_cast(i % properties.getNcolumns()), // NOLINTNEXTLINE(bugprone-integer-division) - .y = static_cast(i / this->properties.getNcolumns())}); + .y = static_cast(i / properties.getNcolumns())}); } } NeutralAtomArchitecture::NeutralAtomArchitecture(const std::string& filename) { - this->loadJson(filename); + loadJson(filename); } -void NeutralAtomArchitecture::computeSwapDistances(qc::fp interactionRadius) { +void NeutralAtomArchitecture::computeSwapDistances( + const qc::fp interactionRadius) { // compute diagonal distances struct DiagonalDistance { std::uint32_t x; @@ -143,9 +178,9 @@ void NeutralAtomArchitecture::computeSwapDistances(qc::fp interactionRadius) { }; std::vector diagonalDistances; - for (uint32_t i = 0; i < this->getNcolumns() && i < interactionRadius; i++) { - for (uint32_t j = i; j < this->getNrows(); j++) { - const auto dist = NeutralAtomArchitecture::getEuclideanDistance( + for (uint32_t i = 0; i < getNcolumns() && i < interactionRadius; i++) { + for (uint32_t j = i; j < getNrows(); j++) { + const auto dist = getEuclideanDistance( Location{.x = 0.0, .y = 0.0}, Location{.x = static_cast(i), .y = static_cast(j)}); if (dist <= interactionRadius) { @@ -170,18 +205,16 @@ void NeutralAtomArchitecture::computeSwapDistances(qc::fp interactionRadius) { }); // compute swap distances - this->swapDistances = - qc::SymmetricMatrix(this->getNpositions()); + swapDistances = qc::SymmetricMatrix(getNpositions()); - for (uint32_t coordIndex1 = 0; coordIndex1 < this->getNpositions(); - coordIndex1++) { + for (uint32_t coordIndex1 = 0; coordIndex1 < getNpositions(); coordIndex1++) { for (uint32_t coordIndex2 = 0; coordIndex2 < coordIndex1; coordIndex2++) { - auto deltaX = this->getManhattanDistanceX(coordIndex1, coordIndex2); - auto deltaY = this->getManhattanDistanceY(coordIndex1, coordIndex2); + auto deltaX = getManhattanDistanceX(coordIndex1, coordIndex2); + auto deltaY = getManhattanDistanceY(coordIndex1, coordIndex2); // check if one can go diagonal to reduce the swap distance int32_t swapDistance = 0; - for (auto& diagonalDistance : + for (const auto& diagonalDistance : std::ranges::reverse_view(diagonalDistances)) { while (deltaX >= diagonalDistance.x && deltaY >= diagonalDistance.y) { swapDistance += 1; @@ -189,44 +222,101 @@ void NeutralAtomArchitecture::computeSwapDistances(qc::fp interactionRadius) { deltaY -= diagonalDistance.y; } } + if (swapDistance == 0) { + swapDistance = 1; + } // save swap distance in matrix - this->swapDistances(coordIndex1, coordIndex2) = swapDistance - 1; - this->swapDistances(coordIndex2, coordIndex1) = swapDistance - 1; + swapDistances(coordIndex1, coordIndex2) = swapDistance - 1; + swapDistances(coordIndex2, coordIndex1) = swapDistance - 1; } } } void NeutralAtomArchitecture::computeNearbyCoordinates() { - this->nearbyCoordinates = std::vector>( - this->getNpositions(), std::set()); - for (CoordIndex coordIndex = 0; coordIndex < this->getNpositions(); - coordIndex++) { + nearbyCoordinates = std::vector(getNpositions(), std::set()); + for (CoordIndex coordIndex = 0; coordIndex < getNpositions(); coordIndex++) { for (CoordIndex otherCoordIndex = 0; otherCoordIndex < coordIndex; otherCoordIndex++) { - if (this->getSwapDistance(coordIndex, otherCoordIndex) == 0) { - this->nearbyCoordinates.at(coordIndex).emplace(otherCoordIndex); - this->nearbyCoordinates.at(otherCoordIndex).emplace(coordIndex); + if (getSwapDistance(coordIndex, otherCoordIndex) == 0) { + nearbyCoordinates.at(coordIndex).emplace(otherCoordIndex); + nearbyCoordinates.at(otherCoordIndex).emplace(coordIndex); } } } } -std::vector NeutralAtomArchitecture::getNN(CoordIndex idx) const { +std::vector +NeutralAtomArchitecture::getNN(const CoordIndex idx) const { std::vector nn; - if (idx % this->getNcolumns() != 0) { + if (idx % getNcolumns() != 0) { nn.emplace_back(idx - 1); } - if (idx % this->getNcolumns() != this->getNcolumns() - 1U) { + if (idx % getNcolumns() != getNcolumns() - 1U) { nn.emplace_back(idx + 1); } - if (idx >= this->getNcolumns()) { - nn.emplace_back(idx - this->getNcolumns()); + if (idx >= getNcolumns()) { + nn.emplace_back(idx - getNcolumns()); } - if (std::cmp_less(idx, this->getNpositions() - this->getNcolumns())) { - nn.emplace_back(idx + this->getNcolumns()); + if (std::cmp_less(idx, getNpositions() - getNcolumns())) { + nn.emplace_back(idx + getNcolumns()); } return nn; } +std::string NeutralAtomArchitecture::getAnimationMachine( + const qc::fp shuttlingSpeedFactor) const { + if (shuttlingSpeedFactor <= 0) { + throw std::runtime_error( + "Shuttling speed factor must be positive, but is " + + std::to_string(shuttlingSpeedFactor)); + } + std::string animationMachine = "name: \"Hybrid_" + name + "\"\n"; + + animationMachine += "movement {\n\tmax_speed: " + + std::to_string(getShuttlingTime(qc::OpType::AodMove) * + shuttlingSpeedFactor) + + "\n}\n"; + + animationMachine += + "time {\n\tload: " + + std::to_string(getShuttlingTime(qc::OpType::AodActivate) / + shuttlingSpeedFactor) + + "\n\tstore: " + + std::to_string(getShuttlingTime(qc::OpType::AodDeactivate) / + shuttlingSpeedFactor) + + "\n\trz: " + std::to_string(getGateTime("x")) + + "\n\try: " + std::to_string(getGateTime("x")) + + "\n\tcz: " + std::to_string(getGateTime("cz")) + "\n\tunit: \"us\"\n}\n"; + + animationMachine += + "distance {\n\tinteraction: " + + std::to_string(getInteractionRadius() * getInterQubitDistance()) + + "\n\tunit: \"um\"\n}\n"; + const auto zoneStart = -getInterQubitDistance(); + const auto zoneEndX = + getNcolumns() * getInterQubitDistance() + getInterQubitDistance(); + const auto zoneEndY = + getNrows() * getInterQubitDistance() + getInterQubitDistance(); + animationMachine += "zone hybrid {\n\tfrom: (" + std::to_string(zoneStart) + + ", " + std::to_string(zoneStart) + ")\n\tto: (" + + std::to_string(zoneEndX) + ", " + + std::to_string(zoneEndY) + ") \n}\n"; + + for (size_t colIdx = 0; colIdx < getNcolumns(); colIdx++) { + for (size_t rowIdx = 0; rowIdx < getNrows(); rowIdx++) { + const auto coordIdx = colIdx + (rowIdx * getNcolumns()); + animationMachine += "trap trap" + std::to_string(coordIdx) + + " {\n\tposition: (" + + std::to_string(static_cast(colIdx) * + getInterQubitDistance()) + + ", " + + std::to_string(static_cast(rowIdx) * + getInterQubitDistance()) + + ")\n}\n"; + } + } + + return animationMachine; +} qc::fp NeutralAtomArchitecture::getOpTime(const qc::Operation* op) const { if (op->getType() == qc::OpType::AodActivate || @@ -234,16 +324,28 @@ qc::fp NeutralAtomArchitecture::getOpTime(const qc::Operation* op) const { return getShuttlingTime(op->getType()); } if (op->getType() == qc::OpType::AodMove) { - const auto v = this->parameters.shuttlingTimes.at(op->getType()); + const auto v = parameters.shuttlingTimes.at(op->getType()); const auto* const opAodMove = dynamic_cast(op); const auto distanceX = opAodMove->getMaxDistance(Dimension::X); const auto distanceY = opAodMove->getMaxDistance(Dimension::Y); return (distanceX + distanceY) / v; } std::string opName; - for (size_t i = 0; i < op->getNcontrols(); ++i) { + const auto nQubits = op->getNqubits(); + for (size_t i = 1; i < nQubits; ++i) { opName += "c"; } + if (op->getType() == qc::OpType::P || op->getType() == qc::OpType::RZ) { + // use time of theta = pi and linearly scale + opName += "z"; + auto param = std::abs(op->getParameter().back()); + constexpr auto twoPi = 2 * std::numbers::pi_v; + param = std::fmod(param, twoPi); + if (param > std::numbers::pi_v) { + param = twoPi - param; // map to [0, pi] + } + return getGateTime(opName) * param / std::numbers::pi_v; + } opName += op->getName(); return getGateTime(opName); } @@ -255,7 +357,8 @@ qc::fp NeutralAtomArchitecture::getOpFidelity(const qc::Operation* op) const { return getShuttlingAverageFidelity(op->getType()); } std::string opName; - for (size_t i = 0; i < op->getNcontrols(); ++i) { + const auto nQubits = op->getNqubits(); + for (size_t i = 1; i < nQubits; ++i) { opName += "c"; } opName += op->getName(); @@ -270,8 +373,12 @@ NeutralAtomArchitecture::getBlockedCoordIndices(const qc::Operation* op) const { return op->getUsedQubits(); } std::set blockedCoordIndices; - for (const auto& coord : op->getUsedQubits()) { - for (uint32_t i = 0; i < getNqubits(); ++i) { + for (auto coord : op->getUsedQubits()) { + // qubits in ancilla register + while (coord >= getNpositions()) { + coord -= getNpositions(); + } + for (uint32_t i = 0; i < getNpositions(); ++i) { if (i == coord) { continue; } @@ -280,6 +387,8 @@ NeutralAtomArchitecture::getBlockedCoordIndices(const qc::Operation* op) const { const auto distance = getEuclideanDistance(coord, i); if (distance <= getBlockingFactor() * getInteractionRadius()) { blockedCoordIndices.emplace(i); + blockedCoordIndices.emplace(i + getNpositions()); + blockedCoordIndices.emplace(i + (2 * getNpositions())); } } } diff --git a/src/hybridmap/NeutralAtomLayer.cpp b/src/hybridmap/NeutralAtomLayer.cpp index 5647d398c..9b6cef126 100644 --- a/src/hybridmap/NeutralAtomLayer.cpp +++ b/src/hybridmap/NeutralAtomLayer.cpp @@ -16,8 +16,7 @@ #include "ir/operations/Operation.hpp" #include -#include -#include +#include #include #include @@ -29,36 +28,10 @@ void NeutralAtomLayer::updateByQubits( candidatesToGates(qubitsToUpdate); } -std::vector NeutralAtomLayer::getIteratorOffset() { - std::vector offset; - offset.reserve(dag.size()); - for (uint32_t i = 0; i < this->dag.size(); ++i) { - offset.emplace_back(static_cast( - std::distance(this->dag[i].begin(), this->iterators[i]))); - } - return offset; -} - -void NeutralAtomLayer::initLayerOffset( - const std::vector& iteratorOffset) { - this->gates.clear(); - for (auto& candidate : this->candidates) { - candidate.clear(); - } - this->mappedSingleQubitGates.clear(); - // if iteratorOffset is empty, set all iterators to begin - if (iteratorOffset.empty()) { - for (uint32_t i = 0; i < this->dag.size(); ++i) { - this->iterators[i] = this->dag[i].begin(); - } - } else { - for (uint32_t i = 0; i < this->dag.size(); ++i) { - this->iterators[i] = this->dag[i].begin() + iteratorOffset[i]; - } - } +void NeutralAtomLayer::initAllQubits() { std::set allQubits; - for (uint32_t i = 0; i < this->dag.size(); ++i) { - allQubits.emplace(i); + for (std::size_t i = 0; i < dag.size(); ++i) { + allQubits.emplace(static_cast(i)); } updateByQubits(allQubits); } @@ -66,30 +39,31 @@ void NeutralAtomLayer::initLayerOffset( void NeutralAtomLayer::updateCandidatesByQubits( const std::set& qubitsToUpdate) { for (const auto& qubit : qubitsToUpdate) { - auto tempIter = iterators[qubit]; - while (tempIter < this->dag[qubit].end()) { - auto* op = (*tempIter)->get(); - if (op->getUsedQubits().size() == 1) { - mappedSingleQubitGates.emplace_back(op); - this->iterators[qubit]++; - tempIter++; - } else { - // continue if following gates commute - bool commutes = true; - while (commutes && tempIter < this->dag[qubit].end()) { - auto* nextOp = (*tempIter)->get(); - commutes = commutesWithAtQubit(gates, nextOp, qubit) && - commutesWithAtQubit(candidates[qubit], nextOp, qubit); - if (commutes) { - if (nextOp->getUsedQubits().size() == 1) { - mappedSingleQubitGates.emplace_back(nextOp); - } else { // not executable but commutes - candidates[qubit].emplace_back(nextOp); - } - } - tempIter++; + if (isFrontLayer) { + while (iterators[qubit] != ends[qubit]) { + auto* op = (*iterators[qubit])->get(); + // check if operation commutes with gates and candidates + const auto commutes = commutesWithAtQubit(gates, op, qubit) && + commutesWithAtQubit(candidates[qubit], op, qubit); + if (commutes) { + candidates[qubit].emplace_back(op); + ++iterators[qubit]; + } else { + break; + } + } + } + // for lookahead layer, take the next k multi-qubit gates + else { + std::size_t multiQubitGatesFound = 0; + while (iterators[qubit] != ends[qubit] && + multiQubitGatesFound < lookaheadDepth) { + auto* op = (*iterators[qubit])->get(); + candidates[qubit].emplace_back(op); + ++iterators[qubit]; + if (op->getUsedQubits().size() > 1) { + multiQubitGatesFound++; } - break; } } } @@ -97,7 +71,9 @@ void NeutralAtomLayer::updateCandidatesByQubits( void NeutralAtomLayer::candidatesToGates( const std::set& qubitsToUpdate) { + newGates.clear(); for (const auto& qubit : qubitsToUpdate) { + // operations moved from candidates to gates have to be removed afterward std::vector toRemove; for (const auto* opPointer : candidates[qubit]) { // check if gate is candidate for all qubits it uses @@ -106,67 +82,60 @@ void NeutralAtomLayer::candidatesToGates( if (qubit == opQubit) { continue; } - if (std::find(candidates[opQubit].begin(), candidates[opQubit].end(), - opPointer) == candidates[opQubit].end()) { + if (std::ranges::find(candidates[opQubit], opPointer) == + candidates[opQubit].end()) { inFrontLayer = false; break; } } if (inFrontLayer) { - this->gates.emplace_back(opPointer); + gates.emplace_back(opPointer); + newGates.emplace_back(opPointer); // remove from candidacy of other qubits for (const auto& opQubit : opPointer->getUsedQubits()) { if (qubit == opQubit) { continue; } - candidates[opQubit].erase(std::find(candidates[opQubit].begin(), - candidates[opQubit].end(), - opPointer)); + candidates[opQubit].erase( + std::ranges::find(candidates[opQubit], opPointer)); } - // save to remove from candidacy of this qubit toRemove.emplace_back(opPointer); } } // remove from candidacy of this qubit // has to be done now to not change iterating list for (const auto* opPointer : toRemove) { - candidates[qubit].erase(std::find(candidates[qubit].begin(), - candidates[qubit].end(), opPointer)); + candidates[qubit].erase(std::ranges::find(candidates[qubit], opPointer)); } } } void NeutralAtomLayer::removeGatesAndUpdate(const GateList& gatesToRemove) { - this->mappedSingleQubitGates.clear(); std::set qubitsToUpdate; for (const auto& gate : gatesToRemove) { - if (std::ranges::find(gates, gate) != gates.end()) { - gates.erase(std::ranges::find(gates, gate)); + const auto it = std::ranges::find(gates, gate); + if (it != gates.end()) { + gates.erase(it); auto usedQubits = gate->getUsedQubits(); qubitsToUpdate.insert(usedQubits.begin(), usedQubits.end()); } } - for (const auto& qubit : qubitsToUpdate) { - ++this->iterators[qubit]; - } updateByQubits(qubitsToUpdate); } // Commutation -bool NeutralAtomLayer::commutesWithAtQubit(const GateList& layer, - const qc::Operation* opPointer, - const qc::Qubit& qubit) { +bool commutesWithAtQubit(const GateList& layer, const qc::Operation* opPointer, + const qc::Qubit& qubit) { return std::ranges::all_of( layer, [&opPointer, &qubit](const auto& frontOpPointer) { return commuteAtQubit(opPointer, frontOpPointer, qubit); }); } -bool NeutralAtomLayer::commuteAtQubit(const qc::Operation* op1, - const qc::Operation* op2, - const qc::Qubit& qubit) { +bool commuteAtQubit(const qc::Operation* op1, const qc::Operation* op2, + const qc::Qubit& qubit) { if (op1->isNonUnitaryOperation() || op2->isNonUnitaryOperation()) { return false; } @@ -180,8 +149,8 @@ bool NeutralAtomLayer::commuteAtQubit(const qc::Operation* op1, } // commutes at qubit if at least one of the two gates does not use qubit - auto usedQubits1 = op1->getUsedQubits(); - auto usedQubits2 = op2->getUsedQubits(); + const auto usedQubits1 = op1->getUsedQubits(); + const auto usedQubits2 = op2->getUsedQubits(); if (!usedQubits1.contains(qubit) || !usedQubits2.contains(qubit)) { return true; } @@ -189,24 +158,20 @@ bool NeutralAtomLayer::commuteAtQubit(const qc::Operation* op1, // for two-qubit gates, check if they commute at qubit // commute if both are controlled at qubit or const Operation* on qubit is // same check controls - if (op1->getControls().find(qubit) != op1->getControls().end() && - op2->getControls().find(qubit) != op2->getControls().end()) { + if (op1->getControls().contains(qubit) && + op2->getControls().contains(qubit)) { return true; } // control and Z also commute - if ((op1->getControls().find(qubit) != op1->getControls().end() && - op2->getType() == qc::OpType::Z) || - (op2->getControls().find(qubit) != op2->getControls().end() && - op1->getType() == qc::OpType::Z)) { + if ((op1->getControls().contains(qubit) && op2->getType() == qc::OpType::Z) || + (op2->getControls().contains(qubit) && op1->getType() == qc::OpType::Z)) { return true; } // check targets - if (std::find(op1->getTargets().begin(), op1->getTargets().end(), qubit) != - op1->getTargets().end() && - (std::find(op2->getTargets().begin(), op2->getTargets().end(), qubit) != - op2->getTargets().end()) && - (op1->getType() == op2->getType())) { + if (std::ranges::find(op1->getTargets(), qubit) != op1->getTargets().end() && + std::ranges::find(op2->getTargets(), qubit) != op2->getTargets().end() && + op1->getType() == op2->getType()) { return true; } return false; diff --git a/src/hybridmap/NeutralAtomScheduler.cpp b/src/hybridmap/NeutralAtomScheduler.cpp index bc8c47a79..538ac8bc7 100644 --- a/src/hybridmap/NeutralAtomScheduler.cpp +++ b/src/hybridmap/NeutralAtomScheduler.cpp @@ -23,66 +23,78 @@ #include #include #include -#include #include #include +#include +#include #include #include #include -na::SchedulerResults -na::NeutralAtomScheduler::schedule(const qc::QuantumComputation& qc, - const std::map& initHwPos, - bool verbose, bool createAnimationCsv, - qc::fp shuttlingSpeedFactor) { +na::SchedulerResults na::NeutralAtomScheduler::schedule( + const qc::QuantumComputation& qc, + const std::map& initHwPos, + const std::map& initFaPos, const bool verbose, + const bool createAnimationCsv, const qc::fp shuttlingSpeedFactor) { + animation.clear(); + animationMachine.clear(); if (verbose) { - std::cout << "\n* schedule start!\n"; + spdlog::info("* schedule start!"); } - std::vector totalExecutionTimes(arch.getNpositions(), 0); - // saves for each coord the time slots that are blocked by a multi qubit gate + const auto nPositions = static_cast(arch->getNpositions()); + const std::size_t numCoords = 3ULL * nPositions; + std::vector totalExecutionTimes(numCoords, qc::fp{0}); std::vector>> rydbergBlockedQubitsTimes( - arch.getNpositions(), std::deque>()); + numCoords); qc::fp aodLastBlockedTime = 0; qc::fp totalGateTime = 0; qc::fp totalGateFidelities = 1; - AnimationAtoms animationAtoms(initHwPos, arch); + std::optional animationAtoms; if (createAnimationCsv) { - animationCsv += animationAtoms.getInitString(); - animationArchitectureCsv = arch.getAnimationCsv(); + animationAtoms.emplace(initHwPos, initFaPos, *arch); + animation += animationAtoms->placeInitAtoms(); + animationMachine = arch->getAnimationMachine(shuttlingSpeedFactor); } int index = 0; - int nAodActivate = 0; + uint32_t nAodActivate = 0; + uint32_t nAodMove = 0; uint32_t nCZs = 0; for (const auto& op : qc) { index++; if (verbose) { - std::cout << "\n" << index << "\n"; + spdlog::info("{}", index); } if (op->getType() == qc::AodActivate) { nAodActivate++; + } else if (op->getType() == qc::AodMove) { + nAodMove++; } else if (op->getType() == qc::OpType::Z && op->getNcontrols() == 1) { nCZs++; } auto qubits = op->getUsedQubits(); - auto opTime = arch.getOpTime(op.get()); + auto opTime = arch->getOpTime(op.get()); if (op->getType() == qc::AodMove || op->getType() == qc::AodActivate || op->getType() == qc::AodDeactivate) { opTime *= shuttlingSpeedFactor; } - auto opFidelity = arch.getOpFidelity(op.get()); + const auto opFidelity = arch->getOpFidelity(op.get()); // DEBUG info if (verbose) { - std::cout << op->getName() << " "; - for (const auto& qubit : qubits) { - std::cout << "q" << qubit << " "; + spdlog::info("{}", op->getName()); + // print control qubits + for (const auto& c : op->getControls()) { + spdlog::info("c{} ", c.qubit); + } + // print target qubits + for (const auto& t : op->getTargets()) { + spdlog::info("q{} ", t); } - std::cout << "-> time: " << opTime << ", fidelity: " << opFidelity - << "\n"; + spdlog::info("-> time: {}, fidelity: {}", opTime, opFidelity); } qc::fp maxTime = 0; @@ -96,7 +108,7 @@ na::NeutralAtomScheduler::schedule(const qc::QuantumComputation& qc, aodLastBlockedTime = maxTime + opTime; } else if (qubits.size() > 1) { // multi qubit gates -> take into consideration blocking - auto rydbergBlockedQubits = arch.getBlockedCoordIndices(op.get()); + auto rydbergBlockedQubits = arch->getBlockedCoordIndices(op.get()); // get max execution time over all blocked qubits bool rydbergBlocked = true; while (rydbergBlocked) { @@ -109,13 +121,13 @@ na::NeutralAtomScheduler::schedule(const qc::QuantumComputation& qc, for (const auto& qubit : rydbergBlockedQubits) { // check if qubit is blocked at maxTime for (const auto& startEnd : rydbergBlockedQubitsTimes[qubit]) { - auto start = startEnd.first; - auto end = startEnd.second; + const auto start = startEnd.first; + const auto end = startEnd.second; if ((start <= maxTime && end > maxTime) || (start <= maxTime + opTime && end > maxTime + opTime)) { rydbergBlocked = true; // update maxTime to the end of the blocking - maxTime = end; + maxTime = std::max(maxTime, end); // remove the blocking break; } @@ -151,61 +163,44 @@ na::NeutralAtomScheduler::schedule(const qc::QuantumComputation& qc, totalGateFidelities *= opFidelity; totalGateTime += opTime; - if (verbose) { - std::cout << "\n"; - printTotalExecutionTimes(totalExecutionTimes, rydbergBlockedQubitsTimes); - } // update animation if (createAnimationCsv) { - animationCsv += - animationAtoms.createCsvOp(op, maxTime, maxTime + opTime, arch); + animation += animationAtoms->opToNaViz(op, maxTime); } } if (verbose) { - std::cout << "\n* schedule end!\n"; - std::cout << "nAodActivate: " << nAodActivate << "\n"; + spdlog::info("* schedule end!"); } const auto maxExecutionTime = *std::ranges::max_element(totalExecutionTimes); const auto totalIdleTime = - maxExecutionTime * arch.getNqubits() - totalGateTime; + maxExecutionTime * arch->getNqubits() - totalGateTime; const auto totalFidelities = totalGateFidelities * - std::exp(-totalIdleTime / arch.getDecoherenceTime()); + std::exp(-totalIdleTime / arch->getDecoherenceTime()); - if (createAnimationCsv) { - animationCsv += animationAtoms.getEndString(maxExecutionTime); - } if (verbose) { printSchedulerResults(totalExecutionTimes, totalIdleTime, - totalGateFidelities, totalFidelities, nCZs); + totalGateFidelities, totalFidelities, nCZs, + nAodActivate, nAodMove); } - return {maxExecutionTime, totalIdleTime, totalGateFidelities, totalFidelities, - nCZs}; + return {maxExecutionTime, totalIdleTime, totalGateFidelities, + totalFidelities, nCZs, nAodActivate, + nAodMove}; } void na::NeutralAtomScheduler::printSchedulerResults( - std::vector& totalExecutionTimes, qc::fp totalIdleTime, - qc::fp totalGateFidelities, qc::fp totalFidelities, uint32_t nCZs) { - auto totalExecutionTime = *std::ranges::max_element(totalExecutionTimes); - std::cout << "\ntotalExecutionTimes: " << totalExecutionTime << "\n"; - std::cout << "totalIdleTime: " << totalIdleTime << "\n"; - std::cout << "totalGateFidelities: " << totalGateFidelities << "\n"; - std::cout << "totalFidelities: " << totalFidelities << "\n"; - std::cout << "totalNumCZs: " << nCZs << "\n"; -} - -void na::NeutralAtomScheduler::printTotalExecutionTimes( - std::vector& totalExecutionTimes, - std::vector>>& blockedQubitsTimes) { - std::cout << "ExecutionTime: " - << "\n"; - for (size_t qubit = 0; qubit < totalExecutionTimes.size(); qubit++) { - std::cout << "[" << qubit << "] " << totalExecutionTimes[qubit] << " \t"; - for (const auto& blockedTime : blockedQubitsTimes[qubit]) { - std::cout << blockedTime.first << "-" << blockedTime.second << " \t"; - } - std::cout << "\n"; - } + std::vector& totalExecutionTimes, const qc::fp totalIdleTime, + const qc::fp totalGateFidelities, const qc::fp totalFidelities, + const uint32_t nCZs, const uint32_t nAodActivate, const uint32_t nAodMove) { + const auto totalExecutionTime = *std::ranges::max_element( + totalExecutionTimes.begin(), totalExecutionTimes.end()); + spdlog::info("totalExecutionTimes: {}", totalExecutionTime); + spdlog::info("totalIdleTime: {}", totalIdleTime); + spdlog::info("totalGateFidelities: {}", totalGateFidelities); + spdlog::info("totalFidelities: {}", totalFidelities); + spdlog::info("totalNumCZs: {}", nCZs); + spdlog::info("nAodActivate: {}", nAodActivate); + spdlog::info("nAodMove: {}", nAodMove); } diff --git a/src/hybridmap/NeutralAtomUtils.cpp b/src/hybridmap/NeutralAtomUtils.cpp index 8d9bcb9a3..9a0157ba2 100644 --- a/src/hybridmap/NeutralAtomUtils.cpp +++ b/src/hybridmap/NeutralAtomUtils.cpp @@ -10,12 +10,19 @@ #include "hybridmap/NeutralAtomUtils.hpp" +#include "circuit_optimizer/CircuitOptimizer.hpp" #include "ir/Definitions.hpp" +#include "ir/QuantumComputation.hpp" +#include "ir/operations/OpType.hpp" +#include "ir/operations/StandardOperation.hpp" #include #include #include #include +#include +#include +#include namespace na { @@ -32,22 +39,26 @@ bool MoveVector::overlap(const MoveVector& other) const { // need to compute all combinations, as sometimes the start and end x/y points // are the same - auto overlapXFirstStart = + const auto overlapXFirstStart = firstStartX >= secondStartX && firstStartX <= secondEndX; - auto overlapXFirstEnd = firstEndX >= secondStartX && firstEndX <= secondEndX; - auto overlapXSecondStart = + const auto overlapXFirstEnd = + firstEndX >= secondStartX && firstEndX <= secondEndX; + const auto overlapXSecondStart = secondStartX >= firstStartX && secondStartX <= firstEndX; - auto overlapXSecondEnd = secondEndX >= firstStartX && secondEndX <= firstEndX; - auto overlapYFirstStart = + const auto overlapXSecondEnd = + secondEndX >= firstStartX && secondEndX <= firstEndX; + const auto overlapYFirstStart = firstStartY >= secondStartY && firstStartY <= secondEndY; - auto overlapYFirstEnd = firstEndY >= secondStartY && firstEndY <= secondEndY; - auto overlapYSecondStart = + const auto overlapYFirstEnd = + firstEndY >= secondStartY && firstEndY <= secondEndY; + const auto overlapYSecondStart = secondStartY >= firstStartY && secondStartY <= firstEndY; - auto overlapYSecondEnd = secondEndY >= firstStartY && secondEndY <= firstEndY; + const auto overlapYSecondEnd = + secondEndY >= firstStartY && secondEndY <= firstEndY; - return (overlapXFirstStart || overlapXFirstEnd || overlapXSecondStart || - overlapXSecondEnd || overlapYFirstStart || overlapYFirstEnd || - overlapYSecondStart || overlapYSecondEnd); + return overlapXFirstStart || overlapXFirstEnd || overlapXSecondStart || + overlapXSecondEnd || overlapYFirstStart || overlapYFirstEnd || + overlapYSecondStart || overlapYSecondEnd; } bool MoveVector::include(const MoveVector& other) const { @@ -60,22 +71,20 @@ bool MoveVector::include(const MoveVector& other) const { const auto secondStartY = std::min(other.yStart, other.yEnd); const auto secondEndY = std::max(other.yStart, other.yEnd); - const auto includeX = - (secondStartX < firstStartX) && (firstEndX < secondEndX); - const auto includeY = - (secondStartY < firstStartY) && (firstEndY < secondEndY); + const auto includeX = secondStartX < firstStartX && firstEndX < secondEndX; + const auto includeY = secondStartY < firstStartY && firstEndY < secondEndY; return includeX || includeY; } -void MoveCombs::addMoveComb(const MoveComb& otherMove) { +void MoveCombs::addMoveComb(const MoveComb& moveComb) { for (auto& comb : moveCombs) { - if (comb == otherMove) { + if (comb == moveComb) { comb.cost = std::numeric_limits::max(); return; } } - moveCombs.emplace_back(otherMove); + moveCombs.emplace_back(moveComb); } void MoveCombs::addMoveCombs(const MoveCombs& otherMoveCombs) { @@ -98,4 +107,85 @@ void MoveCombs::removeLongerMoveCombs() { } } +void BridgeCircuits::computeGates(const size_t length) { + std::vector> hsCzsPerQubit( + bridgeCircuits[length].getNqubits(), {0, 0}); + for (const auto& op : bridgeCircuits[length]) { + if (op->getType() == qc::OpType::H) { + hs[length]++; + hsCzsPerQubit[*op->getTargets().begin()].first++; + } else if (op->getType() == qc::OpType::Z) { + czs[length]++; + hsCzsPerQubit[*op->getUsedQubits().begin()].second++; + hsCzsPerQubit[*op->getUsedQubits().rbegin()].second++; + } + } + // find max depth + const auto maxHcZ = + std::ranges::max_element(hsCzsPerQubit, [](const auto& a, const auto& b) { + return a.first + a.second < b.first + b.second; + }); + hDepth[length] = maxHcZ->first; + czDepth[length] = maxHcZ->second; +} + +void BridgeCircuits::computeBridgeCircuit(const size_t length) { + qc::QuantumComputation qcBridge(3); + qcBridge.cx(0, 1); + qcBridge.cx(1, 2); + qcBridge.cx(0, 1); + qcBridge.cx(1, 2); + + qcBridge = recursiveBridgeIncrease(qcBridge, length - 3); + // convert to CZ on qubit 0 + qcBridge.h(static_cast(qcBridge.getNqubits() - 1)); + qcBridge.insert(qcBridge.begin(), std::make_unique( + qcBridge.getNqubits() - 1, qc::H)); + + qc::CircuitOptimizer::replaceMCXWithMCZ(qcBridge); + qc::CircuitOptimizer::singleQubitGateFusion(qcBridge); + bridgeCircuits[length] = qcBridge; +} + +qc::QuantumComputation +BridgeCircuits::recursiveBridgeIncrease(qc::QuantumComputation qcBridge, + const size_t length) { + if (length == 0) { + return qcBridge; + } + // determine qubit pair with the least amount of gates + std::vector gates(qcBridge.getNqubits() - 1, 0); + for (const auto& gate : qcBridge) { + gates[*gate->getUsedQubits().begin()]++; + } + const auto minIndex = + static_cast(std::ranges::min_element(gates) - gates.begin()); + + qcBridge = bridgeExpand(qcBridge, minIndex); + + return recursiveBridgeIncrease(qcBridge, length - 1); +} +qc::QuantumComputation +BridgeCircuits::bridgeExpand(const qc::QuantumComputation& qcBridge, + const size_t qubit) { + qc::QuantumComputation qcBridgeNew(qcBridge.getNqubits() + 1); + for (const auto& gate : qcBridge) { + const auto usedQubits = gate->getUsedQubits(); + const auto q1 = *usedQubits.begin(); + const auto q2 = *usedQubits.rbegin(); + if (q1 == qubit && q2 == qubit + 1) { + qcBridgeNew.cx(q1, q2); + qcBridgeNew.cx(q1 + 1, q2 + 1); + qcBridgeNew.cx(q1, q2); + qcBridgeNew.cx(q1 + 1, q2 + 1); + } else if (*usedQubits.begin() > qubit) { + // shift qubits by one + qcBridgeNew.cx(q1 + 1, q2 + 1); + } else { + qcBridgeNew.cx(q1, q2); + } + } + return qcBridgeNew; +} + } // namespace na diff --git a/test/hybridmap/architectures/arch_minimal.json b/test/hybridmap/architectures/arch_minimal.json new file mode 100644 index 000000000..9c3b4c0a5 --- /dev/null +++ b/test/hybridmap/architectures/arch_minimal.json @@ -0,0 +1,38 @@ +{ + "name": "minimal", + "properties": { + "nRows": 1, + "nColumns": 2, + "nAods": 1, + "nAodCoordinates": 1, + "interQubitDistance": 3, + "minimalAodDistance": 0.1, + "interactionRadius": 1, + "blockingFactor": 1 + }, + "parameters": { + "nQubits": 2, + "gateTimes": { + "none": 0.5 + }, + "gateAverageFidelities": { + "none": 0.999 + }, + "decoherenceTimes": { + "t1": 100000000, + "t2": 1500000 + }, + "shuttlingTimes": { + "move": 0.55, + "aod_move": 0.55, + "aod_activate": 20, + "aod_deactivate": 20 + }, + "shuttlingAverageFidelities": { + "move": 1, + "aod_move": 1, + "aod_activate": 1, + "aod_deactivate": 1 + } + } +} diff --git a/test/hybridmap/architectures/rubidium.json b/test/hybridmap/architectures/rubidium_gate.json similarity index 92% rename from test/hybridmap/architectures/rubidium.json rename to test/hybridmap/architectures/rubidium_gate.json index 60a9de429..eb33e8dd3 100644 --- a/test/hybridmap/architectures/rubidium.json +++ b/test/hybridmap/architectures/rubidium_gate.json @@ -6,7 +6,7 @@ "nAods": 1, "nAodCoordinates": 5, "interQubitDistance": 3, - "minimalAodDistance": 0.1, + "minimalAodDistance": 1.5, "interactionRadius": 1.5, "blockingFactor": 1 }, @@ -53,8 +53,8 @@ "shuttlingAverageFidelities": { "move": 1, "aod_move": 1, - "aod_activate": 1, - "aod_deactivate": 1 + "aod_activate": 0.98, + "aod_deactivate": 0.98 } } } diff --git a/test/hybridmap/architectures/rubidium_shuttling.json b/test/hybridmap/architectures/rubidium_shuttling.json index 451603912..421575c0c 100644 --- a/test/hybridmap/architectures/rubidium_shuttling.json +++ b/test/hybridmap/architectures/rubidium_shuttling.json @@ -11,7 +11,7 @@ "blockingFactor": 1 }, "parameters": { - "nQubits": 11, + "nQubits": 12, "gateTimes": { "none": 0.5, "rx": 0.5, diff --git a/test/hybridmap/circuits/long_random.qasm b/test/hybridmap/circuits/long_random.qasm new file mode 100644 index 000000000..50471a55c --- /dev/null +++ b/test/hybridmap/circuits/long_random.qasm @@ -0,0 +1,224 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[12]; + +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; +cx q[7],q[3]; +cx q[1],q[8]; +cx q[4],q[10]; +cx q[11],q[2]; +cx q[9],q[6]; +cx q[0],q[5]; +cx q[10],q[1]; +cx q[8],q[11]; +cx q[2],q[0]; +cx q[6],q[4]; +cx q[3],q[9]; +cx q[5],q[7]; +cx q[11],q[10]; +cx q[8],q[9]; +cx q[0],q[3]; +cx q[4],q[2]; +cx q[1],q[7]; +cx q[6],q[5]; +cx q[9],q[0]; +cx q[10],q[8]; diff --git a/test/hybridmap/circuits/multi_qubit.qasm b/test/hybridmap/circuits/multi_qubit.qasm new file mode 100644 index 000000000..27510d869 --- /dev/null +++ b/test/hybridmap/circuits/multi_qubit.qasm @@ -0,0 +1,4 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[10]; +ccccx q[0], q[1], q[2], q[3], q[4]; diff --git a/test/hybridmap/test_architecture.cpp b/test/hybridmap/test_architecture.cpp new file mode 100644 index 000000000..e0c1d36db --- /dev/null +++ b/test/hybridmap/test_architecture.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "hybridmap/NeutralAtomArchitecture.hpp" +#include "hybridmap/NeutralAtomDefinitions.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace na; + +TEST(NeutralAtomArchitectureMethods, GetNNTest) { + const auto arch = + NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + const auto cols = arch.getNcolumns(); + const auto rows = arch.getNrows(); + + // Top-left corner (0): neighbors are right (1) and down (cols) when available + const auto nn0 = arch.getNN(0); + if (cols > 1) { + EXPECT_NE(std::ranges::find(nn0, static_cast(1)), nn0.end()); + } + if (rows > 1) { + EXPECT_NE(std::ranges::find(nn0, cols), nn0.end()); + } + // No negative indices + EXPECT_EQ(std::ranges::find(nn0, static_cast(-1)), nn0.end()); +} + +TEST(NeutralAtomArchitectureMethods, GetIndexRoundTrip) { + const auto arch = + NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + // Round-trip index -> coordinate -> index + const CoordIndex idx = 3 < arch.getNpositions() ? 3 : 0; + const auto coord = arch.getCoordinate(idx); + const auto idxBack = arch.getIndex(coord); + EXPECT_EQ(idxBack, idx); +} + +TEST(NeutralAtomArchitectureMethods, AnimationAPIsProduceContent) { + const auto arch = + NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + + // getAnimationMachine should return a non-empty string with known sections + const auto machine = arch.getAnimationMachine(1.0); + EXPECT_FALSE(machine.empty()); + EXPECT_NE(machine.find("movement"), std::string::npos); + EXPECT_NE(machine.find("time"), std::string::npos); + EXPECT_NE(machine.find("zone hybrid"), std::string::npos); + EXPECT_NE(machine.find("trap"), std::string::npos); + + // Save to temp files + const std::filesystem::path tmpMachine = + std::filesystem::temp_directory_path() / "arch_anim_machine.csv"; + arch.saveAnimationMachine(tmpMachine.string(), 1.0); + // Verify files exist and are non-empty + ASSERT_TRUE(std::filesystem::exists(tmpMachine)); + EXPECT_GT(std::filesystem::file_size(tmpMachine), 0U); + // Cleanup best-effort + std::error_code ec; + std::filesystem::remove(tmpMachine, ec); +} + +TEST(NeutralAtomArchitectureMethods, BasicCountsAndOffsetDistance) { + const auto arch = + NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + // Simple sanity for counts + EXPECT_GT(arch.getNAods(), 0); + EXPECT_GT(arch.getNAodCoordinates(), 0); + + // offset distance * levels ~= inter-qubit distance + const auto off = arch.getOffsetDistance(); + const auto levels = arch.getNAodIntermediateLevels(); + EXPECT_GT(levels, 0); + EXPECT_NEAR(off * levels, arch.getInterQubitDistance(), 1e-9); +} + +TEST(NeutralAtomArchitectureExceptions, NonexistentFileThrows) { + EXPECT_THROW((void)NeutralAtomArchitecture( + "architectures/this_file_does_not_exist.json"), + std::runtime_error); +} + +TEST(NeutralAtomArchitectureExceptions, ParseInvalidJsonThrows) { + const auto tmp = std::filesystem::temp_directory_path() / "invalid_arch.json"; + { + std::ofstream ofs(tmp); + ofs << "{ invalid json }"; + } + EXPECT_THROW((void)NeutralAtomArchitecture(tmp.string()), std::runtime_error); + std::error_code ec; + std::filesystem::remove(tmp, ec); +} + +TEST(NeutralAtomArchitectureExceptions, TooManyQubitsThrows) { + const auto tmp = + std::filesystem::temp_directory_path() / "too_many_qubits.json"; + // Minimal JSON: 1x1 positions but nQubits = 2 -> should throw + { + const auto* content = R"JSON({ + "name": "test", + "properties": { + "nRows": 1, + "nColumns": 1, + "nAods": 1, + "nAodCoordinates": 1, + "interQubitDistance": 1.0, + "interactionRadius": 1.0, + "blockingFactor": 1.0, + "minimalAodDistance": 1.0 + }, + "parameters": { + "nQubits": 2 + } + })JSON"; + std::ofstream ofs(tmp); + ofs << content; + } + EXPECT_THROW((void)NeutralAtomArchitecture(tmp.string()), std::runtime_error); + std::error_code ec; + std::filesystem::remove(tmp, ec); +} + +TEST(NeutralAtomArchitectureExceptions, GetGateTimeFallbackToNone) { + const auto arch = + NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + const auto fallback = arch.getGateTime("none"); + const auto missing = arch.getGateTime("this_gate_name_should_not_exist"); + EXPECT_DOUBLE_EQ(missing, fallback); + const auto fallbackFid = arch.getGateAverageFidelity("none"); + const auto missingFid = + arch.getGateAverageFidelity("this_gate_name_should_not_exist"); + EXPECT_DOUBLE_EQ(missingFid, fallbackFid); +} diff --git a/test/hybridmap/test_hardware_qubits.cpp b/test/hybridmap/test_hardware_qubits.cpp new file mode 100644 index 000000000..bbbbdf51b --- /dev/null +++ b/test/hybridmap/test_hardware_qubits.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "hybridmap/HardwareQubits.hpp" +#include "hybridmap/NeutralAtomArchitecture.hpp" +#include "hybridmap/NeutralAtomDefinitions.hpp" +#include "hybridmap/NeutralAtomUtils.hpp" + +#include +#include +#include +#include + +namespace { + +TEST(HardwareQubitsExceptions, AccessEmptyCoordThrows) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + na::HardwareQubits const hw(arch, 2, na::InitialCoordinateMapping::Trivial, + 0); + + constexpr auto emptyCoord = static_cast(3); + EXPECT_THROW(static_cast(hw.getHwQubit(emptyCoord)), + std::runtime_error); +} + +TEST(HardwareQubitsExceptions, MoveInvalidCoordinateThrows) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + na::HardwareQubits hw(arch, /*nQubits*/ 2, + na::InitialCoordinateMapping::Trivial, /*seed*/ 0); + + // Coordinate equal to Npositions is out of range + const auto invalidCoord = arch.getNpositions(); + EXPECT_THROW(hw.move(/*hwQubit*/ 0, invalidCoord), std::runtime_error); +} + +TEST(HardwareQubitsExceptions, MoveToOccupiedCoordinateThrows) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + na::HardwareQubits hw(arch, /*nQubits*/ 2, + na::InitialCoordinateMapping::Trivial, /*seed*/ 0); + + const auto occupied = hw.getCoordIndex(/*hwQubit*/ 1); + EXPECT_THROW(hw.move(/*hwQubit*/ 0, occupied), std::runtime_error); +} + +TEST(HardwareQubitsBehavior, RemoveHwQubitRemovesMappingsAndNeighbors) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + na::HardwareQubits hw(arch, /*nQubits*/ 3, + na::InitialCoordinateMapping::Trivial, /*seed*/ 0); + + // Remove middle qubit (1) and verify it is no longer addressable + hw.removeHwQubit(/*hwQubit*/ 1); + + // getCoordIndex should throw since the qubit was erased from the mapping + EXPECT_THROW((void)hw.getCoordIndex(1), std::out_of_range); + + // Remaining qubits neighbor lists should not contain the removed qubit + for (na::HwQubit const q : {0U, 2U}) { + const auto neighbors = hw.getNearbyQubits(q); + EXPECT_TRUE(!neighbors.contains(1U)); + } +} + +TEST(HardwareQubitsBehavior, RandomInitializationIsDeterministicPerSeed) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + constexpr na::CoordIndex nQ = 4; + + na::HardwareQubits const hw(arch, nQ, na::InitialCoordinateMapping::Random, + /*seed*/ 0); + + // All assigned coordinates are unique and within bounds + std::set coords; + for (na::HwQubit q = 0; q < nQ; ++q) { + const auto c = hw.getCoordIndex(q); + EXPECT_LT(c, arch.getNpositions()); + coords.insert(c); + } + EXPECT_EQ(coords.size(), static_cast(nQ)); +} + +} // namespace diff --git a/test/hybridmap/test_hybrid_synthesis_map.cpp b/test/hybridmap/test_hybrid_synthesis_map.cpp new file mode 100644 index 000000000..951f19c11 --- /dev/null +++ b/test/hybridmap/test_hybrid_synthesis_map.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +// +// This file is part of the MQT QMAP library released under the MIT license. +// See README.md or go to https://github.com/cda-tum/qmap for more information. +// + +#include "hybridmap/HybridSynthesisMapper.hpp" +#include "hybridmap/NeutralAtomArchitecture.hpp" +#include "hybridmap/NeutralAtomUtils.hpp" +#include "ir/QuantumComputation.hpp" + +#include +#include +#include +#include +#include + +namespace na { +class TestParametrizedHybridSynthesisMapper + : public testing::TestWithParam { +protected: + std::string testArchitecturePath = "architectures/"; + std::vector circuits; + + void SetUp() override { + testArchitecturePath += GetParam() + ".json"; + qc::QuantumComputation qc1(3); + qc1.x(0); + qc1.cx(0, 1); + qc1.cx(1, 2); + circuits.push_back(qc1); + + qc::QuantumComputation qc2(3); + qc2.move(0, 2); + qc2.x(0); + circuits.push_back(qc2); + } + + // Test the HybridSynthesisMapper class +}; + +TEST_P(TestParametrizedHybridSynthesisMapper, AdjacencyMatrix) { + const auto arch = NeutralAtomArchitecture(testArchitecturePath); + auto mapper = HybridSynthesisMapper(arch); + mapper.initMapping(3); + auto adjMatrix = mapper.getCircuitAdjacencyMatrix(); + EXPECT_EQ(adjMatrix.size(), 3); + EXPECT_TRUE(adjMatrix(0, 2) == 0 || adjMatrix(0, 2) == 1); +} + +TEST_P(TestParametrizedHybridSynthesisMapper, EvaluateSynthesisStep) { + const auto arch = NeutralAtomArchitecture(testArchitecturePath); + auto params = MapperParameters(); + params.verbose = true; + auto mapper = HybridSynthesisMapper(arch, params); + // Intentionally not initializing the mapper to test error handling + EXPECT_THROW(static_cast(mapper.getCircuitAdjacencyMatrix()), + std::runtime_error); + // Initializing with too many qubits to test error handling + EXPECT_THROW(mapper.initMapping(50), std::runtime_error); + const auto best = mapper.evaluateSynthesisSteps(circuits, true); + EXPECT_EQ(best.size(), 2); + EXPECT_GE(best[0], 0); + EXPECT_GE(best[1], 0); +} + +INSTANTIATE_TEST_SUITE_P(HybridSynthesisMapperTestSuite, + TestParametrizedHybridSynthesisMapper, + ::testing::Values("rubidium_gate", "rubidium_hybrid", + "rubidium_shuttling")); + +class TestHybridSynthesisMapper : public testing::Test { +protected: + NeutralAtomArchitecture arch = + NeutralAtomArchitecture("architectures/rubidium_gate.json"); + HybridSynthesisMapper mapper = HybridSynthesisMapper(arch); + qc::QuantumComputation qc; + + void SetUp() override { + qc = qc::QuantumComputation(3); + qc.x(0); + qc.cx(0, 1); + qc.cx(1, 2); + + mapper.initMapping(3); + } +}; + +TEST_F(TestHybridSynthesisMapper, DirectlyMap) { + mapper.appendWithoutMapping(qc); + const auto synthesizedQc = mapper.getSynthesizedQc(); + EXPECT_EQ(synthesizedQc.getNqubits(), 3); + EXPECT_EQ(synthesizedQc.getNops(), 3); +} + +TEST_F(TestHybridSynthesisMapper, completelyRemap) { + mapper.appendWithoutMapping(qc); + mapper.appendWithoutMapping(qc); + const auto mappedQc = mapper.getMappedQc(); + EXPECT_EQ(mappedQc.getNqubitsWithoutAncillae(), arch.getNpositions()); + EXPECT_GE(mappedQc.getNops(), 3); + + mapper.completeRemap(Identity); + const auto mappedQcRemapped = mapper.getMappedQc(); + EXPECT_EQ(mappedQcRemapped.getNqubitsWithoutAncillae(), arch.getNpositions()); + EXPECT_GE(mappedQcRemapped.getNops(), 3); +} + +TEST_F(TestHybridSynthesisMapper, MapAppend) { + mapper.appendWithMapping(qc); + const auto synthesizedQc = mapper.getSynthesizedQc(); + EXPECT_EQ(synthesizedQc.getNqubits(), 3); + EXPECT_GE(synthesizedQc.getNops(), 3); +} + +TEST_F(TestHybridSynthesisMapper, Output) { + mapper.appendWithMapping(qc); + const auto qasm = mapper.getSynthesizedQcQASM(); + EXPECT_FALSE(qasm.empty()); + const auto tempDir = std::filesystem::temp_directory_path(); + const auto qasmPath = tempDir / "test_output.qasm"; + mapper.saveSynthesizedQc(qasmPath.string()); + EXPECT_TRUE(std::filesystem::exists(qasmPath)); + EXPECT_GT(std::filesystem::file_size(qasmPath), 0); + std::filesystem::remove(qasmPath); +} + +} // namespace na diff --git a/test/hybridmap/test_hybridmap.cpp b/test/hybridmap/test_hybridmap.cpp index f69ffecba..dd76561c3 100644 --- a/test/hybridmap/test_hybridmap.cpp +++ b/test/hybridmap/test_hybridmap.cpp @@ -19,11 +19,11 @@ #include #include #include +#include #include #include -class NeutralAtomArchitectureTest - : public ::testing::TestWithParam { +class NeutralAtomArchitectureTest : public testing::TestWithParam { protected: std::string testArchitecturePath = "architectures/"; @@ -32,14 +32,14 @@ class NeutralAtomArchitectureTest TEST_P(NeutralAtomArchitectureTest, LoadArchitectures) { std::cout << "wd: " << std::filesystem::current_path() << '\n'; - auto arch = na::NeutralAtomArchitecture(testArchitecturePath); + const auto arch = na::NeutralAtomArchitecture(testArchitecturePath); // Test get properties EXPECT_LE(arch.getNqubits(), arch.getNpositions()); EXPECT_EQ(arch.getNpositions(), arch.getNrows() * arch.getNcolumns()); // Test precomputed values - auto c1 = arch.getCoordinate(0); - auto c2 = arch.getCoordinate(1); + const auto c1 = arch.getCoordinate(0); + const auto c2 = arch.getCoordinate(1); EXPECT_GE(arch.getSwapDistance(c1, c2), 0); EXPECT_GE(arch.getNAodIntermediateLevels(), 1); // Test get parameters @@ -48,19 +48,19 @@ TEST_P(NeutralAtomArchitectureTest, LoadArchitectures) { // Test distance functions EXPECT_GE(arch.getEuclideanDistance(c1, c2), 0); // Test MoveVector functions - auto mv = arch.getVector(0, 1); + const auto mv = arch.getVector(0, 1); EXPECT_GE(arch.getVectorShuttlingTime(mv), 0); } INSTANTIATE_TEST_SUITE_P(NeutralAtomArchitectureTestSuite, NeutralAtomArchitectureTest, - ::testing::Values("rubidium", "rubidium_hybrid", - "rubidium_shuttling")); -class NeutralAtomMapperTest + ::testing::Values("rubidium_gate", "rubidium_hybrid", + "arch_minimal")); +class NeutralAtomMapperTestParams // parameters are architecture, circuit, gateWeight, shuttlingWeight, - // lookAheadWeight, initialCoordinateMapping - : public ::testing::TestWithParam< - std::tuple> { protected: std::string testArchitecturePath = "architectures/"; @@ -68,43 +68,51 @@ class NeutralAtomMapperTest qc::fp gateWeight = 1; qc::fp shuttlingWeight = 1; qc::fp lookAheadWeight = 1; + qc::fp dynamicMappingWeight = 1; na::InitialCoordinateMapping initialCoordinateMapping = - na::InitialCoordinateMapping::Trivial; + na::InitialCoordinateMapping::Random; // fixed qc::fp decay = 0.1; qc::fp shuttlingTimeWeight = 0.1; uint32_t seed = 42; void SetUp() override { - auto params = GetParam(); + const auto& params = GetParam(); testArchitecturePath += std::get<0>(params) + ".json"; testQcPath += std::get<1>(params) + ".qasm"; gateWeight = std::get<2>(params); shuttlingWeight = std::get<3>(params); lookAheadWeight = std::get<4>(params); - initialCoordinateMapping = std::get<5>(params); + dynamicMappingWeight = std::get<5>(params); + initialCoordinateMapping = std::get<6>(params); } }; -TEST_P(NeutralAtomMapperTest, MapCircuitsIdentity) { +TEST_P(NeutralAtomMapperTestParams, MapCircuitsIdentity) { const auto arch = na::NeutralAtomArchitecture(testArchitecturePath); constexpr na::InitialMapping initialMapping = na::InitialMapping::Identity; na::NeutralAtomMapper mapper(arch); na::MapperParameters mapperParameters; - mapperParameters.initialMapping = initialCoordinateMapping; + mapperParameters.initialCoordMapping = initialCoordinateMapping; mapperParameters.lookaheadWeightSwaps = lookAheadWeight; mapperParameters.lookaheadWeightMoves = lookAheadWeight; mapperParameters.decay = decay; mapperParameters.shuttlingTimeWeight = shuttlingTimeWeight; mapperParameters.gateWeight = gateWeight; mapperParameters.shuttlingWeight = shuttlingWeight; + mapperParameters.dynamicMappingWeight = dynamicMappingWeight; mapperParameters.seed = seed; mapperParameters.verbose = true; + mapperParameters.maxBridgeDistance = 2; + mapperParameters.numFlyingAncillas = 1; + mapperParameters.usePassBy = false; + mapperParameters.limitShuttlingLayer = 1; mapper.setParameters(mapperParameters); auto qc = qasm3::Importer::importf(testQcPath); - auto qcMapped = mapper.map(qc, initialMapping); - auto qcAodMapped = mapper.convertToAod(qcMapped); + const auto qcMapped = mapper.map(qc, initialMapping); + ASSERT_GE(qcMapped.size(), qc.size()); + mapper.convertToAod(); const auto scheduleResults = mapper.schedule(true, true); @@ -114,46 +122,169 @@ TEST_P(NeutralAtomMapperTest, MapCircuitsIdentity) { } INSTANTIATE_TEST_SUITE_P( - NeutralAtomMapperTestSuite, NeutralAtomMapperTest, + NeutralAtomMapperTestSuite, NeutralAtomMapperTestParams, ::testing::Combine( - ::testing::Values("rubidium", "rubidium_hybrid", "rubidium_shuttling"), + ::testing::Values("rubidium_gate", "rubidium_hybrid"), ::testing::Values("dj_nativegates_rigetti_qiskit_opt3_10", "modulo_2", "multiply_2", "qft_nativegates_rigetti_qiskit_opt3_10", "random_nativegates_rigetti_qiskit_opt3_10"), ::testing::Values(1, 0.), ::testing::Values(1, 0.), - ::testing::Values(0, 0.1), - ::testing::Values(na::InitialCoordinateMapping::Trivial, - na::InitialCoordinateMapping::Random))); + ::testing::Values(0.1), ::testing::Values(0), + ::testing::Values(na::InitialCoordinateMapping::Trivial))); -TEST(NeutralAtomMapperTest, Output) { - const auto arch = - na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); - constexpr na::InitialMapping initialMapping = na::InitialMapping::Identity; - na::NeutralAtomMapper mapper(arch); +class NeutralAtomMapperTest : public testing::Test { +protected: + std::string testArchitecturePath = "architectures/rubidium_shuttling.json"; + const na::NeutralAtomArchitecture arch = + na::NeutralAtomArchitecture(testArchitecturePath); + na::InitialMapping const initialMapping = na::InitialMapping::Graph; na::MapperParameters mapperParameters; - mapperParameters.initialMapping = na::InitialCoordinateMapping::Trivial; - mapperParameters.lookaheadWeightSwaps = 0.1; - mapperParameters.lookaheadWeightMoves = 0.1; - mapperParameters.decay = 0; - mapperParameters.shuttlingTimeWeight = 0.1; - mapperParameters.gateWeight = 1; - mapperParameters.shuttlingWeight = 0; - mapperParameters.seed = 43; - mapperParameters.verbose = true; - mapper.setParameters(mapperParameters); + na::NeutralAtomMapper mapper{arch, mapperParameters}; + qc::QuantumComputation qc; - auto qc = qasm3::Importer::importf( - "circuits/dj_nativegates_rigetti_qiskit_opt3_10.qasm"); + void SetUp() override { + mapperParameters.initialCoordMapping = + na::InitialCoordinateMapping::Trivial; + mapperParameters.lookaheadDepth = 1; + mapperParameters.lookaheadWeightSwaps = 0.1; + mapperParameters.lookaheadWeightMoves = 0.5; + mapperParameters.decay = 0; + mapperParameters.shuttlingTimeWeight = 0.1; + mapperParameters.gateWeight = 1; + mapperParameters.shuttlingWeight = 0; + mapperParameters.seed = 43; + mapperParameters.verbose = false; + mapperParameters.numFlyingAncillas = 1; + mapperParameters.limitShuttlingLayer = 1; + mapperParameters.usePassBy = true; + mapper = na::NeutralAtomMapper(arch, mapperParameters); + qc = qasm3::Importer::importf( + "circuits/dj_nativegates_rigetti_qiskit_opt3_10.qasm"); + } +}; + +TEST_F(NeutralAtomMapperTest, Output) { auto qcMapped = mapper.map(qc, initialMapping); + // write to file + const auto tempDir = std::filesystem::temp_directory_path(); + const auto qasmPath = tempDir / "test.qasm"; + mapper.saveMappedQcQasm(qasmPath.string()); + const auto qcMappedFromFile = mapper.getMappedQcQasm(); + EXPECT_GT(qcMappedFromFile.size(), 0); + EXPECT_TRUE(std::filesystem::exists(qasmPath)); + EXPECT_GT(std::filesystem::file_size(qasmPath), 0); + std::filesystem::remove(qasmPath); - qcMapped.dumpOpenQASM(std::cout, false); + const auto aodQasmPath = tempDir / "test_aod.qasm"; + mapper.saveMappedQcAodQasm(aodQasmPath.string()); + const auto qcMappedAod = mapper.getMappedQcAodQasm(); + EXPECT_GT(qcMappedAod.size(), 0); + EXPECT_TRUE(std::filesystem::exists(aodQasmPath)); + EXPECT_GT(std::filesystem::file_size(aodQasmPath), 0); + std::filesystem::remove(aodQasmPath); - auto qcAodMapped = mapper.convertToAod(qcMapped); - qcAodMapped.dumpOpenQASM(std::cout, false); + const auto mapperStats = mapper.getStats(); + EXPECT_GE(mapperStats.nSwaps + mapperStats.nBridges + mapperStats.nFAncillas + + mapperStats.nMoves + mapperStats.nPassBy, + 0); + const auto mapperStatsMap = mapper.getStatsMap(); + EXPECT_GE(mapperStatsMap.at("nSwaps") + mapperStatsMap.at("nBridges") + + mapperStatsMap.at("nFAncillas") + mapperStatsMap.at("nMoves") + + mapperStatsMap.at("nPassBy"), + 0); + const auto initHwPos = mapper.getInitHwPos(); + EXPECT_EQ(initHwPos.size(), arch.getNqubits() - 1 /* flying ancilla */); + + const auto scheduleResults = mapper.schedule(true, true); + const auto animationViz = mapper.getAnimationViz(); + EXPECT_GT(animationViz.size(), 0); + const auto animationPath = tempDir / "test"; + mapper.saveAnimationFiles(animationPath.string()); + const auto machinePath = animationPath.string() + ".namachine"; + const auto vizPath = animationPath.string() + ".naviz"; + EXPECT_TRUE(std::filesystem::exists(machinePath)); + EXPECT_GT(std::filesystem::file_size(machinePath), 0); + std::filesystem::remove(machinePath); + EXPECT_TRUE(std::filesystem::exists(vizPath)); + EXPECT_GT(std::filesystem::file_size(vizPath), 0); + std::filesystem::remove(vizPath); - auto scheduleResults = mapper.schedule(true, true); std::cout << scheduleResults.toCsv(); ASSERT_GT(scheduleResults.totalFidelities, 0); } + +// Exception tests for HybridNeutralAtomMapper + +TEST(NeutralAtomMapperExceptions, NotEnoughQubitsForCircuitAndAncillas) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_hybrid.json"); + na::MapperParameters p; + p.initialCoordMapping = na::InitialCoordinateMapping::Trivial; + p.shuttlingWeight = 0; // avoid free-coords check interference + p.numFlyingAncillas = 1; // allowed by ctor + na::NeutralAtomMapper mapper(arch, p); + + // Circuit uses exactly all hardware qubits; +1 ancilla should trigger + qc::QuantumComputation qc1((arch.getNqubits())); + EXPECT_THROW((void)mapper.map(qc1, na::InitialMapping::Identity), + std::runtime_error); + + // Circuit bigger than architecture should throw + qc::QuantumComputation qc2(arch.getNqubits() + 1); + EXPECT_THROW((void)mapper.map(qc2, na::InitialMapping::Identity), + std::runtime_error); +} + +// for now, only one flying ancilla is supported +TEST(NeutralAtomMapperExceptions, OnlyOneFlyingAncillaSupported) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_hybrid.json"); + na::MapperParameters p; + p.initialCoordMapping = na::InitialCoordinateMapping::Trivial; + p.shuttlingWeight = 0; // avoid free-coords check interference + p.numFlyingAncillas = 2; // should be rejected + EXPECT_THROW((void)na::NeutralAtomMapper(arch, p), std::runtime_error); +} + +TEST(NeutralAtomMapperExceptions, NoFreeCoordsForShuttlingConstructor) { + // Create minimal arch JSON: 1x1 positions, nQubits = 1 => no free coords + const auto arch = + na::NeutralAtomArchitecture("architectures/arch_minimal.json"); + + na::MapperParameters p; + p.initialCoordMapping = na::InitialCoordinateMapping::Trivial; + p.shuttlingWeight = 1.0; // triggers constructor check + EXPECT_THROW((void)na::NeutralAtomMapper(arch, p), std::runtime_error); + + na::MapperParameters p1 = p; + p1.shuttlingWeight = 0.0; // construct ok + na::NeutralAtomMapper mapper(arch, p1); + p1.shuttlingWeight = 0.5; // triggers setParameters check + EXPECT_THROW(mapper.setParameters(p1), std::runtime_error); +} +TEST(NeutralAtomMapperExceptions, NoMultiQubitSpace) { + // Test that mapping throws when multi-qubit gates cannot be executed + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_gate.json"); + constexpr na::MapperParameters p; + na::NeutralAtomMapper mapper(arch, p); + qc::QuantumComputation qc = + qasm3::Importer::importf("circuits/multi_qubit.qasm"); + EXPECT_THROW(static_cast(mapper.map(qc, na::InitialMapping::Identity)), + std::runtime_error); +} + +TEST(NeutralAtomMapperExceptions, LongShuttling) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + na::MapperParameters p; + p.gateWeight = 0.0; + p.verbose = true; + na::NeutralAtomMapper mapper(arch, p); + qc::QuantumComputation qc = + qasm3::Importer::importf("circuits/long_random.qasm"); + const auto circ = mapper.map(qc, na::InitialMapping::Graph); + mapper.convertToAod(); +} diff --git a/test/hybridmap/test_mapping.cpp b/test/hybridmap/test_mapping.cpp new file mode 100644 index 000000000..8d9c09f66 --- /dev/null +++ b/test/hybridmap/test_mapping.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "hybridmap/HardwareQubits.hpp" +#include "hybridmap/Mapping.hpp" +#include "hybridmap/NeutralAtomArchitecture.hpp" +#include "hybridmap/NeutralAtomUtils.hpp" +#include "ir/QuantumComputation.hpp" + +#include +#include + +// These tests focus only on exception behavior in Mapping (mapping.cpp/.hpp). + +TEST(MappingExceptions, CircuitExceedsHardwareThrows) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + // Hardware has 1 available logical qubit, circuit needs 2 + na::HardwareQubits const hw( + arch, /*nQubits*/ 1, na::InitialCoordinateMapping::Trivial, /*seed*/ 0); + qc::QuantumComputation qc(2); + + EXPECT_THROW((void)na::Mapping(2, na::InitialMapping::Identity, qc, hw), + std::runtime_error); +} + +TEST(MappingExceptions, GetCircQubitThrowsIfHardwareNotMapped) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + // Hardware has 4 logical spots, but circuit uses only 2 (identity mapping) + na::HardwareQubits const hw( + arch, /*nQubits*/ 4, na::InitialCoordinateMapping::Trivial, /*seed*/ 0); + qc::QuantumComputation qc(2); + na::Mapping const m(2, na::InitialMapping::Identity, qc, hw); + + // hw qubits 0 and 1 are mapped; 2 and 3 are not -> getCircQubit(2) should + // throw + EXPECT_THROW((void)m.getCircQubit(2), std::runtime_error); +} + +TEST(MappingExceptions, ApplySwapThrowsIfBothHardwareQubitsUnmapped) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + // Hardware has 4, circuit maps only 2 via identity + na::HardwareQubits const hw( + arch, /*nQubits*/ 4, na::InitialCoordinateMapping::Trivial, /*seed*/ 0); + qc::QuantumComputation qc(2); + na::Mapping m(2, na::InitialMapping::Identity, qc, hw); + + // Swap two unmapped hardware qubits (2 and 3) -> should throw + EXPECT_THROW(m.applySwap({2, 3}), std::runtime_error); +} + +TEST(MappingExceptions, GetHwQubitThrowsOutOfRangeForInvalidCircuitIndex) { + const auto arch = + na::NeutralAtomArchitecture("architectures/rubidium_shuttling.json"); + na::HardwareQubits const hw( + arch, /*nQubits*/ 2, na::InitialCoordinateMapping::Trivial, /*seed*/ 0); + qc::QuantumComputation qc(2); + na::Mapping const m(2, na::InitialMapping::Identity, qc, hw); + + // Access circuit qubit index outside [0, nQubits) -> std::out_of_range + EXPECT_THROW((void)m.getHwQubit(2), std::out_of_range); +} diff --git a/test/hybridmap/test_scheduler.cpp b/test/hybridmap/test_scheduler.cpp new file mode 100644 index 000000000..94d8f62ab --- /dev/null +++ b/test/hybridmap/test_scheduler.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "hybridmap/NeutralAtomScheduler.hpp" + +#include + +TEST(NeutralAtomSchedulerTests, SchedulerResultsToMapForPython) { + // Given some arbitrary scheduler result values + na::SchedulerResults const res(/*executionTime*/ 10.5, + /*idleTime*/ 2.0, + /*gateFidelities*/ 0.9, + /*fidelities*/ 0.85, + /*nCZs*/ 3, + /*nAodActivate*/ 4, + /*nAodMove*/ 5); + + const auto m = res.toMap(); + + // Only the documented keys are exported + ASSERT_EQ(m.size(), 7U); + EXPECT_TRUE(m.count("totalExecutionTime")); + EXPECT_TRUE(m.count("totalIdleTime")); + EXPECT_TRUE(m.count("totalGateFidelities")); + EXPECT_TRUE(m.count("totalFidelities")); + EXPECT_TRUE(m.count("nCZs")); + EXPECT_TRUE(m.count("nAodActivate")); + EXPECT_TRUE(m.count("nAodMove")); + + // Values preserved exactly + EXPECT_DOUBLE_EQ(m.at("totalExecutionTime"), 10.5); + EXPECT_DOUBLE_EQ(m.at("totalIdleTime"), 2.0); + EXPECT_DOUBLE_EQ(m.at("totalGateFidelities"), 0.9); + EXPECT_DOUBLE_EQ(m.at("totalFidelities"), 0.85); + EXPECT_DOUBLE_EQ(m.at("nCZs"), 3.0); + EXPECT_DOUBLE_EQ(m.at("nAodActivate"), 4.0); + EXPECT_DOUBLE_EQ(m.at("nAodMove"), 5.0); +} diff --git a/test/hybridmap/test_utils.cpp b/test/hybridmap/test_utils.cpp new file mode 100644 index 000000000..e5a4dbba4 --- /dev/null +++ b/test/hybridmap/test_utils.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "hybridmap/NeutralAtomDefinitions.hpp" +#include "hybridmap/NeutralAtomUtils.hpp" + +#include +#include + +using namespace na; + +TEST(NeutralAtomUtils, InitialCoordinateMappingFromStringTest) { + EXPECT_EQ(initialCoordinateMappingFromString("trivial"), + InitialCoordinateMapping::Trivial); + EXPECT_EQ(initialCoordinateMappingFromString("0"), + InitialCoordinateMapping::Trivial); + EXPECT_EQ(initialCoordinateMappingFromString("random"), + InitialCoordinateMapping::Random); + EXPECT_EQ(initialCoordinateMappingFromString("1"), + InitialCoordinateMapping::Random); +} + +TEST(NeutralAtomUtils, InitialCoordinateMappingFromStringThrow) { + EXPECT_THROW((void)initialCoordinateMappingFromString("foobar"), + std::invalid_argument); +} + +TEST(NeutralAtomUtils, InitialMappingFromString) { + EXPECT_EQ(initialMappingFromString("identity"), InitialMapping::Identity); + EXPECT_EQ(initialMappingFromString("0"), InitialMapping::Identity); + EXPECT_EQ(initialMappingFromString("graph"), InitialMapping::Graph); + EXPECT_EQ(initialMappingFromString("1"), InitialMapping::Graph); +} + +TEST(NeutralAtomUtils, InitialMappingFromStringThrow) { + EXPECT_THROW((void)initialMappingFromString("baz"), std::invalid_argument); +} + +TEST(NeutralAtomUtils, MoveCombConstructorsAndEquality) { + constexpr AtomMove m1{.c1 = 1, .c2 = 2, .load1 = true, .load2 = false}; + constexpr AtomMove m2{.c1 = 3, .c2 = 4, .load1 = false, .load2 = true}; + const CoordIndices pos{5, 6}; + + // vector-based constructor + const MoveComb cvec({m1, m2}, /*cost*/ 1.23, /*op*/ nullptr, pos); + EXPECT_FALSE(cvec.empty()); + EXPECT_EQ(cvec.moves.size(), 2U); + EXPECT_DOUBLE_EQ(cvec.cost, 1.23); + EXPECT_EQ(cvec.op, nullptr); + EXPECT_EQ(cvec.bestPos, pos); + + // single-move constructor + const MoveComb cone(m1, /*cost*/ 0.5, /*op*/ nullptr, CoordIndices{7}); + EXPECT_FALSE(cone.empty()); + EXPECT_EQ(cone.moves.size(), 1U); + EXPECT_DOUBLE_EQ(cone.cost, 0.5); + EXPECT_EQ(cone.op, nullptr); + EXPECT_EQ(cone.bestPos, (CoordIndices{7})); + + // equality compares only the moves vector (per operator== definition) + const MoveComb cvecSameMoves( + {m1, m2}, /*cost*/ 9.99, + /*op*/ reinterpret_cast(0x1), CoordIndices{42}); + EXPECT_TRUE(cvec == cvecSameMoves); + EXPECT_FALSE(cvec != cvecSameMoves); + + const MoveComb cvecDiffMoves({m2, m1}, /*cost*/ 1.23, /*op*/ nullptr, pos); + EXPECT_FALSE(cvec == cvecDiffMoves); + EXPECT_TRUE(cvec != cvecDiffMoves); +} + +TEST(NeutralAtomUtils, MoveCombEmpty) { + const MoveComb emptyComb; + EXPECT_TRUE(emptyComb.empty()); +} + +TEST(NeutralAtomUtils, MoveVectorLengthAndDirection) { + // 3-4-5 triangle + const MoveVector mv(0, 0, 3, 4); + EXPECT_DOUBLE_EQ(mv.getLength(), 5.0); + // Direction should be positive in both axes + EXPECT_TRUE(mv.direction.x); + EXPECT_TRUE(mv.direction.y); +} + +TEST(NeutralAtomUtils, MoveVectorOverlapAndInclude) { + // Overlap: same row (y=0), x-ranges sharing points + const MoveVector a(0, 0, 5, 0); + const MoveVector b(3, 0, 10, 0); + EXPECT_TRUE(a.overlap(b)); + EXPECT_TRUE(b.overlap(a)); + EXPECT_TRUE(a.sameDirection(b)); + EXPECT_TRUE(b.sameDirection(a)); + + // No overlap in either X or Y ranges -> should be false + const MoveVector c(0, 0, 0, 2); // vertical at x=0, y in [0,2] + const MoveVector d(10, 5, 12, 5); // horizontal at y=5, x in [10,12] + EXPECT_FALSE(c.overlap(d)); + EXPECT_FALSE(d.overlap(c)); + + // Include + const MoveVector inner(2, 0, 4, 0); + const MoveVector outer(1, 0, 5, 0); + EXPECT_TRUE(inner.include(outer)); + EXPECT_FALSE(outer.include(inner)); + + // Non-include: disjoint segments + const MoveVector e(0, 0, 2, 0); + const MoveVector f(3, 0, 5, 0); + EXPECT_FALSE(e.include(f)); + EXPECT_FALSE(f.include(e)); +} diff --git a/test/python/hybrid_mapper/test_hybrid_mapper.py b/test/python/hybrid_mapper/test_hybrid_mapper.py index dd598a0a6..55005d40f 100644 --- a/test/python/hybrid_mapper/test_hybrid_mapper.py +++ b/test/python/hybrid_mapper/test_hybrid_mapper.py @@ -15,7 +15,7 @@ import pytest from mqt.core import load -from mqt.qmap.hybrid_mapper import HybridMapperParameters, HybridNAMapper, NeutralAtomHybridArchitecture +from mqt.qmap.hybrid_mapper import HybridNAMapper, MapperParameters, NeutralAtomHybridArchitecture arch_dir = Path(__file__).parent.parent.parent / "hybridmap" / "architectures" circuit_dir = Path(__file__).parent.parent.parent / "hybridmap" / "circuits" @@ -34,7 +34,7 @@ @pytest.mark.parametrize( "arch_filename", [ - "rubidium.json", + "rubidium_gate.json", "rubidium_hybrid.json", "rubidium_shuttling.json", ], @@ -48,18 +48,20 @@ def test_hybrid_na_mapper( """Test the hybrid Neutral Atom mapper.""" arch = NeutralAtomHybridArchitecture(str(arch_dir / arch_filename)) - params = HybridMapperParameters( - lookahead_weight_moves=lookahead_weight, - lookahead_weight_swaps=lookahead_weight, - decay=decay, - gate_weight=gate_shuttling_weight, - ) + params = MapperParameters() + params.lookahead_weight_moves = lookahead_weight + params.lookahead_weight_swaps = lookahead_weight + params.decay = decay + params.gate_weight = gate_shuttling_weight mapper = HybridNAMapper(arch, params=params) - qc = load(circuit_dir / circuit_filename) - - mapper.map(qc) + # Map directly from QASM file using the pybind-exposed convenience method + mapper.map_qasm_file(str(circuit_dir / circuit_filename)) results = mapper.schedule(create_animation_csv=False) + # Validate QASM exports (mapped abstract and AOD-annotated) + mapped_qasm = mapper.get_mapped_qc_qasm() + # AOD QASM retrieval currently not exposed in Python API; just sanity check mapped_qasm + assert mapped_qasm.strip() assert results["totalExecutionTime"] > 0 assert results["totalIdleTime"] > 0 @@ -68,8 +70,8 @@ def test_hybrid_na_mapper( def _nested_mapper_create() -> HybridNAMapper: """Create a nested Neutral Atom hybrid architecture.""" - arch = NeutralAtomHybridArchitecture(str(arch_dir / "rubidium.json")) - params = HybridMapperParameters() + arch = NeutralAtomHybridArchitecture(str(arch_dir / "rubidium_gate.json")) + params = MapperParameters() return HybridNAMapper(arch, params=params) @@ -81,6 +83,8 @@ def test_keep_alive() -> None: mapper.map(qc) results = mapper.schedule(create_animation_csv=False) + mapped_qasm = mapper.get_mapped_qc_qasm() + assert mapped_qasm.strip() assert results["totalExecutionTime"] > 0 assert results["totalIdleTime"] > 0 diff --git a/test/python/hybrid_mapper/test_hybrid_synthesis.py b/test/python/hybrid_mapper/test_hybrid_synthesis.py new file mode 100644 index 000000000..33c97cfbb --- /dev/null +++ b/test/python/hybrid_mapper/test_hybrid_synthesis.py @@ -0,0 +1,131 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Test the hybrid Neutral Atom synthesis mapping.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest +from mqt.core import load +from qiskit import QuantumCircuit + +from mqt.qmap.hybrid_mapper import HybridSynthesisMapper, NeutralAtomHybridArchitecture + +arch_dir = Path(__file__).parent.parent.parent / "hybridmap" / "architectures" +circuit_dir = Path(__file__).parent.parent.parent / "hybridmap" / "circuits" + +qc1_qiskit = QuantumCircuit(3) +qc1_qiskit.h(0) +qc1_qiskit.cx(0, 1) +qc1_qiskit.cx(1, 2) +qc1 = load(qc1_qiskit) + +qc2_qiskit = QuantumCircuit(3) +qc2_qiskit.cx(0, 2) +qc2_qiskit.cx(1, 2) +qc2 = load(qc2_qiskit) + + +@pytest.mark.parametrize( + "arch_filename", + [ + "rubidium_gate.json", + "rubidium_hybrid.json", + "rubidium_shuttling.json", + ], +) +def test_hybrid_synthesis(arch_filename: str) -> None: + """Test the hybrid Neutral Atom synthesis mapper evaluation of different circuits.""" + arch = NeutralAtomHybridArchitecture(str(arch_dir / arch_filename)) + + synthesis_mapper = HybridSynthesisMapper(arch) + synthesis_mapper.init_mapping(3) + best_circuit = synthesis_mapper.evaluate_synthesis_steps( + [qc1, qc2], + also_map=True, + ) + + assert isinstance(best_circuit, list) + assert len(best_circuit) == 2 + assert best_circuit[0] <= 1 + assert best_circuit[0] >= 0 + + +@pytest.mark.parametrize( + "arch_filename", + [ + "rubidium_gate.json", + "rubidium_hybrid.json", + "rubidium_shuttling.json", + ], +) +def test_hybrid_synthesis_input_output(arch_filename: str, tmp_path: Path) -> None: + """Test printing and saving the produced circuits.""" + arch = NeutralAtomHybridArchitecture(str(arch_dir / arch_filename)) + synthesis_mapper = HybridSynthesisMapper(arch) + synthesis_mapper.init_mapping(3) + + synthesis_mapper.append_with_mapping(qc1) + synthesis_mapper.append_without_mapping(qc2) + + qasm = synthesis_mapper.get_mapped_qc_qasm() + assert qasm is not None + + filename_mapped = tmp_path / f"{arch_filename}_mapped.qasm" + synthesis_mapper.save_mapped_qc_qasm(str(filename_mapped)) + + synthesis_mapper.convert_to_aod() + qasm_aod = synthesis_mapper.get_mapped_qc_aod_qasm() + assert qasm_aod is not None + + filename_mapped_aod = tmp_path / f"{arch_filename}_mapped_aod.qasm" + synthesis_mapper.save_mapped_qc_aod_qasm(str(filename_mapped_aod)) + + qasm_synth = synthesis_mapper.get_synthesized_qc_qasm() + assert qasm_synth is not None + + filename_synth = tmp_path / f"{arch_filename}_synthesized.qasm" + synthesis_mapper.save_synthesized_qc_qasm(str(filename_synth)) + + +def test_adjacency_matrix() -> None: + """Test the adjacency matrix of the hybrid Neutral Atom synthesis mapper.""" + arch = NeutralAtomHybridArchitecture(str(arch_dir / "rubidium_gate.json")) + synthesis_mapper = HybridSynthesisMapper(arch) + circ_size = 3 + synthesis_mapper.init_mapping(circ_size) + synthesis_mapper.append_with_mapping(qc1) + adj_mat = np.array(synthesis_mapper.get_circuit_adjacency_matrix()) + assert adj_mat is not None + assert adj_mat.shape == (circ_size, circ_size) + for i in range(circ_size): + for j in range(circ_size): + assert adj_mat[i, j] == adj_mat[j, i] + + +def help_create_arch(arch_filename: str) -> NeutralAtomHybridArchitecture: + """Helper function to create a hybrid Neutral Atom architecture.""" + return NeutralAtomHybridArchitecture(str(arch_dir / arch_filename)) + + +def help_create_mapper(arch_filename: str) -> HybridSynthesisMapper: + """Helper function to create a hybrid synthesis mapper.""" + arch = help_create_arch(arch_filename) + synthesis_mapper = HybridSynthesisMapper(arch) + synthesis_mapper.init_mapping(3) + return synthesis_mapper + + +def test_keep_alive() -> None: + """Test the keep alive functionality of the hybrid Neutral Atom synthesis mapper.""" + synthesis_mapper = help_create_mapper("rubidium_gate.json") + synthesis_mapper.append_with_mapping(qc1) + _ = synthesis_mapper.get_circuit_adjacency_matrix()