diff --git a/.agent/plans/qft-adder.md b/.agent/plans/qft-adder.md new file mode 100644 index 0000000000..8d2a58fd7e --- /dev/null +++ b/.agent/plans/qft-adder.md @@ -0,0 +1,39 @@ +# QFT adder benchmark + +Status: complete. + +## Goal and scope + +One `qft-adder` family adds equal-width, big-endian operands. Register mode +returns `addend || sum`; constant mode uses precomputed phases and returns the +sum alone. Wrap mode keeps the operand width; carry mode adds one sum bit. +Leading zeros determine width. Only register addends support `+` qubits; +accumulators and constant addends are binary. Arbitrary amplitudes are excluded. + +`src/bench/QFTAdder.cpp` owns input validation and analytic results. +`mlir/bench/programs/QFTAdder.cpp` owns preparation and both circuit methods. +JSON, bindings, and the CLI share the family catalog and typed options. + +## Decisions + +Keep the register and constant implementations as methods because they perform +related arithmetic with different qubit and gate costs. Use one overflow policy +for both. Retain the shared no-swap QFT helpers and bounded f64 phase tables. +Sum width is limited to 1024 bits to retain representable phases and reference +weights. Group repeated preparation bits into structured loops. + +Measure the register addend as well as the sum to expose their correlation. A +unique logical result exists only for basis inputs. Sampling alone cannot check +phase coherence; compare coherent DD statevectors with exact amplitudes. + +## Validation + +Run the native benchmark and generation binaries and +`uv run --no-sync pytest test/python/bench test/python/test_cli.py -k bench`. +The native generation tests check all 336 operand pairs across widths one +through three, both methods, and both overflow policies, plus 28 coherent +register-addition statevectors. Python checks typed options, JSON round trips, +and generated program bindings. The implementation and its arithmetic and +phase-sensitive execution tests are in #2404. Local validation passed all 24 +native generation tests and 17 Python benchmark/CLI tests, plus lint and full +C++ lint. Coverage is bounded to these widths and supported inputs. diff --git a/CHANGELOG.md b/CHANGELOG.md index cc5a590700..3b9aca651f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ releases may include breaking changes. - ✨ Add a library for typed structured quantum benchmarks with versioned instance specifications, analytic references, deterministic manifests, and C++, Python, and command-line interfaces ([#2135], [#2299], [#2315], [#2324], - [#2337], [#2380], [#2402]) ([**@burgholzer**], [**@denialhaag**]) + [#2337], [#2380], [#2402], [#2404]) ([**@burgholzer**], [**@denialhaag**]) - ✨ Add DD construction, simulation, statevector extraction, and sampling for QCO programs with structured control and dynamic quantum data, including direct lowering and dense-array helpers for supported compiler inputs @@ -927,6 +927,7 @@ for previous changelogs._ [#2421]: https://github.com/munich-quantum-toolkit/core/pull/2421 +[#2404]: https://github.com/munich-quantum-toolkit/core/pull/2404 [#2402]: https://github.com/munich-quantum-toolkit/core/pull/2402 [#2399]: https://github.com/munich-quantum-toolkit/core/pull/2399 [#2380]: https://github.com/munich-quantum-toolkit/core/pull/2380 diff --git a/bindings/bench/CMakeLists.txt b/bindings/bench/CMakeLists.txt index f3caed2f84..e5d58009eb 100644 --- a/bindings/bench/CMakeLists.txt +++ b/bindings/bench/CMakeLists.txt @@ -14,6 +14,7 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}-bench-bindings) register_grover.cpp register_multiplexer.cpp register_qft.cpp + register_qft_adder.cpp register_qpe.cpp register_teleportation.cpp) diff --git a/bindings/bench/register_bench.cpp b/bindings/bench/register_bench.cpp index 03f5b51122..b48e7b77fd 100644 --- a/bindings/bench/register_bench.cpp +++ b/bindings/bench/register_bench.cpp @@ -24,6 +24,7 @@ void registerGHZ(const nb::module_& m); void registerGrover(const nb::module_& m); void registerMultiplexer(const nb::module_& m); void registerQFT(const nb::module_& m); +void registerQFTAdder(const nb::module_& m); void registerQPE(const nb::module_& m); void registerTeleportation(const nb::module_& m); @@ -69,6 +70,10 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { m.def_submodule("qft", "QFT benchmark instances and options."); registerQFT(qft); + const nb::module_ qftAdder = m.def_submodule( + "qft_adder", "QFT adder benchmark instances and options."); + registerQFTAdder(qftAdder); + const nb::module_ qpe = m.def_submodule("qpe", "QPE benchmark instances and options."); registerQPE(qpe); diff --git a/bindings/bench/register_qft_adder.cpp b/bindings/bench/register_qft_adder.cpp new file mode 100644 index 0000000000..f42754b7db --- /dev/null +++ b/bindings/bench/register_qft_adder.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "bench/JSON.hpp" +#include "bench/QFTAdder.hpp" + +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) + +#include + +namespace mqt { + +namespace nb = nanobind; +using namespace nb::literals; + +/// NOLINTNEXTLINE(misc-use-internal-linkage) +void registerQFTAdder(const nb::module_& m) { + nb::enum_(m, "Method", + "How the addend enters the circuit.") + .value("REGISTER", bench::QFTAdderMethod::Register) + .value("CONSTANT", bench::QFTAdderMethod::Constant); + nb::enum_(m, "Overflow", + "Wrap the sum or retain carry.") + .value("WRAP", bench::QFTAdderOverflow::Wrap) + .value("CARRY", bench::QFTAdderOverflow::Carry); + nb::class_(m, "Options", + "Parameters for a QFT adder.") + .def(nb::init(), + nb::kw_only(), "addend"_a, "accumulator"_a, + "method"_a = bench::QFTAdderMethod::Register, + "overflow"_a = bench::QFTAdderOverflow::Wrap) + .def_ro( + "addend", &bench::QFTAdderOptions::addend, + R"pb(Big-endian addend; register inputs also accept ``+`` for a :math:`|+\rangle` qubit.)pb") + .def_ro("accumulator", &bench::QFTAdderOptions::accumulator, + "Binary accumulator with the same width as the addend.") + .def_ro("method", &bench::QFTAdderOptions::method, + "Register or constant addition.") + .def_ro("overflow", &bench::QFTAdderOptions::overflow, + "Wrap or carry behavior."); + + auto qftAdder = nb::class_( + m, "QFTAdder", + R"pb(Add equal-width operands with an exact no-swap QFT circuit. + +Register addition uses controlled phases and returns the addend followed by the +sum. Constant addition compiles the addend into phases and returns only the sum. +Wrap mode computes :math:`(a + b) \bmod 2^n`; carry mode retains one extra sum bit. +All strings are big-endian, and leading zeros determine the operand width. + +Register addends may contain ``+`` for independent :math:`|+\rangle` qubits. +The accumulator and constant addends must be binary. The circuit follows +Draper's register addition and its constant-input Fourier specialization.)pb"); + qftAdder.def(nb::init(), "options"_a) + .def_prop_ro("options", &bench::QFTAdder::options, + nb::rv_policy::reference_internal, + "The resolved benchmark parameters.") + .def_prop_ro("output", &bench::QFTAdder::output, + nb::rv_policy::reference_internal, + "The logical output register.") + .def_prop_ro( + "expected_result", &bench::QFTAdder::expectedResult, + "The unique logical outcome, or None for a superposed addend.") + .def("probability", &bench::QFTAdder::probability, "outcome"_a, + "Return the ideal probability of an outcome.") + .def("evaluate", &bench::QFTAdder::evaluate, "counts"_a, + "Compare sampled counts with the ideal distribution.") + .def( + "generate", + [](const bench::QFTAdder& value) { + return nb::module_::import_("mqt.core.mlir") + .attr("_generate_benchmark")( + bench::toInstanceSpecificationJSON(value)); + }, + nb::sig("def generate(self) -> mqt.core.mlir.QCProgram"), + "Generate the benchmark as a QC program.") + .def_prop_ro( + "instance_specification_json", + [](const bench::QFTAdder& value) { + return bench::toInstanceSpecificationJSON(value); + }, + "The canonical instance specification JSON.") + .def_prop_ro( + "manifest_json", + [](const bench::QFTAdder& value) { + return bench::toManifestJSON(value); + }, + "The canonical manifest JSON.") + .def_prop_ro( + "case_id", + [](const bench::QFTAdder& value) { return bench::caseId(value); }, + "The stable semantic case ID.") + .def_static("from_instance_specification_json", + &bench::qftAdderFromInstanceSpecificationJSON, "json"_a, + nb::kw_only(), "source"_a = "", + "Parse a strict benchmark instance specification.") + .def_static("from_manifest_json", &bench::qftAdderFromManifestJSON, + "json"_a, nb::kw_only(), "source"_a = "", + "Parse a strict benchmark manifest."); +} + +} // namespace mqt diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 9634dfffbe..bdcf42b6ac 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -236,3 +236,35 @@ does not depend on a path or output format. Parsing a manifest checks its resolved parameters, logical output, reference, definition version, and case ID. Before evaluation, normalize backend results to the manifest's big-endian `result` order. + +## Benchmark families + +### QFT addition + +The `qft-adder` family adds two equal-width operands. `REGISTER` stores the +addend in qubits and applies controlled phases; `CONSTANT` combines the known +addend into one phase per accumulator qubit. Both use the same exact QFT and +inverse QFT. `WRAP` keeps an n-bit sum, while `CARRY` keeps an extra sum bit. + +```{code-cell} ipython3 +from mqt.core import mlir +from mqt.core.bench import qft_adder + +adder = qft_adder.QFTAdder( + qft_adder.Options( + addend="110", + accumulator="011", + method=qft_adder.Method.CONSTANT, + overflow=qft_adder.Overflow.CARRY, + ) +) +assert mlir.sample(adder.generate(), shots=128, seed=17) == {"1001": 128} +``` + +Operands are big-endian strings; leading zeros set their common width. The +accumulator and constant addends must be binary. Register addends may also use +`+` for independently prepared $|+\rangle$ qubits, such as `addend="1+0"`. +Register results concatenate the addend and sum so their correlation remains +observable. Constant results contain only the sum. `expected_result` is the +unique logical outcome for basis inputs and `None` for a superposed addend. The +total sum width, including an optional carry bit, is limited to 1024. diff --git a/include/mqt-core/bench/BenchmarkFamilies.inc b/include/mqt-core/bench/BenchmarkFamilies.inc index 6657eaed60..716647c7fc 100644 --- a/include/mqt-core/bench/BenchmarkFamilies.inc +++ b/include/mqt-core/bench/BenchmarkFamilies.inc @@ -30,6 +30,7 @@ MQT_BENCHMARK_FAMILY(GHZ, ghz, "ghz", 1) MQT_BENCHMARK_FAMILY(Grover, grover, "grover", 1) MQT_BENCHMARK_FAMILY(Multiplexer, multiplexer, "multiplexer", 1) MQT_BENCHMARK_FAMILY(QFT, qft, "qft", 1) +MQT_BENCHMARK_FAMILY(QFTAdder, qftAdder, "qft-adder", 1) MQT_BENCHMARK_FAMILY(QPE, qpe, "qpe", 1) MQT_BENCHMARK_FAMILY(Teleportation, teleportation, "teleportation", 1) diff --git a/include/mqt-core/bench/JSON.hpp b/include/mqt-core/bench/JSON.hpp index 0d3f148768..3e2351c8df 100644 --- a/include/mqt-core/bench/JSON.hpp +++ b/include/mqt-core/bench/JSON.hpp @@ -16,6 +16,7 @@ #include "bench/Grover.hpp" #include "bench/Multiplexer.hpp" #include "bench/QFT.hpp" +#include "bench/QFTAdder.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" #include "bench/mqt_core_bench_export.h" diff --git a/include/mqt-core/bench/QFTAdder.hpp b/include/mqt-core/bench/QFTAdder.hpp new file mode 100644 index 0000000000..b875649068 --- /dev/null +++ b/include/mqt-core/bench/QFTAdder.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "bench/Evaluation.hpp" +#include "bench/mqt_core_bench_export.h" + +#include +#include +#include +#include +#include + +namespace mqt::bench { + +/// Store the addend in a quantum register or compile it into constant phases. +enum class QFTAdderMethod : uint8_t { Register, Constant }; +/// Wrap the sum at the operand width or retain an extra carry bit. +enum class QFTAdderOverflow : uint8_t { Wrap, Carry }; + +/// Parameters for a QFT adder benchmark. +struct QFTAdderOptions { + static constexpr size_t MAX_QUBITS = 1'024; + + /// Big-endian addend; register inputs also allow '+' for a |+> qubit. + std::string addend; + /// Big-endian binary accumulator, with the same width as the addend. + std::string accumulator; + QFTAdderMethod method = QFTAdderMethod::Register; + QFTAdderOverflow overflow = QFTAdderOverflow::Wrap; +}; + +/// Add two configured operands using an exact no-swap QFT circuit. +/// Register results concatenate the addend and sum; constant results contain +/// only the sum. Carry mode extends the sum by one bit. All strings are +/// big-endian; leading zeros determine the operand width. +class MQT_CORE_BENCH_EXPORT QFTAdder final { +public: + explicit QFTAdder(QFTAdderOptions options); + + [[nodiscard]] const QFTAdderOptions& options() const noexcept; + [[nodiscard]] const Output& output() const noexcept; + /// Return the unique logical outcome, or no value for a superposed addend. + [[nodiscard]] const std::optional& + expectedResult() const noexcept; + /// Return the ideal probability of a logical outcome. + [[nodiscard]] double probability(std::string_view outcome) const; + /// Compare sampled logical outcomes with the ideal distribution. + [[nodiscard]] Evaluation evaluate(const Counts& counts) const; + +private: + QFTAdderOptions options_; + Output output_; + std::optional expectedResult_; +}; + +} // namespace mqt::bench diff --git a/mlir/bench/programs/CMakeLists.txt b/mlir/bench/programs/CMakeLists.txt index d16f06aa00..8fdd8a24b3 100644 --- a/mlir/bench/programs/CMakeLists.txt +++ b/mlir/bench/programs/CMakeLists.txt @@ -13,6 +13,8 @@ add_library( Grover.cpp Multiplexer.cpp QFT.cpp + QFTAdder.cpp + QFTUtils.cpp QPE.cpp Teleportation.cpp) target_link_libraries(MQTBenchmarkPrograms PUBLIC MQT::CoreBench MLIRQCProgramBuilder diff --git a/mlir/bench/programs/Programs.h b/mlir/bench/programs/Programs.h index fa78d9ce7d..00f23daddb 100644 --- a/mlir/bench/programs/Programs.h +++ b/mlir/bench/programs/Programs.h @@ -23,6 +23,7 @@ class GHZ; class Grover; class Multiplexer; class QFT; +class QFTAdder; class QPE; class Teleportation; } // namespace mqt::bench @@ -48,6 +49,10 @@ SmallVector multiplexer(qc::QCProgramBuilder& builder, /// Emit one configured QFT benchmark. SmallVector qft(qc::QCProgramBuilder& builder, const QFT& benchmark); +/// Emit one configured QFT adder benchmark. +SmallVector qftAdder(qc::QCProgramBuilder& builder, + const QFTAdder& benchmark); + /// Emit one configured QPE benchmark. SmallVector qpe(qc::QCProgramBuilder& builder, const QPE& benchmark); diff --git a/mlir/bench/programs/QFT.cpp b/mlir/bench/programs/QFT.cpp index da9dfddc32..6e4deec2d0 100644 --- a/mlir/bench/programs/QFT.cpp +++ b/mlir/bench/programs/QFT.cpp @@ -11,13 +11,11 @@ #include "bench/QFT.hpp" #include "Programs.h" +#include "QFTUtils.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include -#include -#include #include -#include #include #include @@ -27,22 +25,6 @@ namespace mqt::bench { using namespace mlir; -static void -qftPhaseRotationLoop(qc::QCProgramBuilder& builder, Value lower, Value upper, - const double start, const double factor, - const function_ref& body) { - auto one = builder.indexConstant(1); - auto first = builder.floatConstant(start); - auto scale = builder.floatConstant(factor); - auto loop = scf::ForOp::create(builder, lower, upper, one, ValueRange{first}); - OpBuilder::InsertionGuard guard(builder); - builder.setInsertionPointToStart(loop.getBody()); - auto angle = loop.getRegionIterArg(0); - body(angle, loop.getInductionVar()); - auto next = arith::MulFOp::create(builder, angle, scale).getResult(); - scf::YieldOp::create(builder, ValueRange{next}); -} - [[nodiscard]] static SmallVector standardQFT(qc::QCProgramBuilder& builder, const QFT& benchmark) { const auto& options = benchmark.options(); @@ -55,21 +37,9 @@ standardQFT(qc::QCProgramBuilder& builder, const QFT& benchmark) { builder.scfFor(period, qubits, 1, [&](Value index) { builder.h(builder.loadQubit(query, index)); }); - auto zero = builder.indexConstant(0); - auto one = builder.indexConstant(1); + detail::forwardQFT(builder, query, qubits); + auto last = builder.indexConstant(qubits - 1); - builder.scfFor(0, qubits, 1, [&](Value step) { - auto target = arith::SubIOp::create(builder, last, step); - builder.h(builder.loadQubit(query, target)); - auto previous = arith::SubIOp::create(builder, target, one); - qftPhaseRotationLoop(builder, zero, target, std::numbers::pi / 2.0, 0.5, - [&](Value angle, Value distance) { - auto control = arith::SubIOp::create( - builder, previous, distance); - builder.cp(angle, builder.loadQubit(query, control), - builder.loadQubit(query, target)); - }); - }); builder.scfFor(0, qubits, 1, [&](Value index) { auto resultIndex = arith::SubIOp::create(builder, last, index); builder.measure(builder.loadQubit(query, index), result, resultIndex); @@ -89,20 +59,22 @@ semiclassicalQFT(qc::QCProgramBuilder& builder, const QFT& benchmark) { auto total = builder.indexConstant(qubits); auto one = builder.indexConstant(1); auto active = builder.indexConstant(qubits - period); + auto firstAngle = builder.floatConstant(std::numbers::pi / 2.); + auto half = builder.floatConstant(0.5); - const auto round = [&](Value step, const bool preparePlus) { + const auto round = [&](Value index, const bool preparePlus) { if (preparePlus) { builder.h(query); } - auto previous = arith::SubIOp::create(builder, step, one); - qftPhaseRotationLoop( - builder, zero, step, std::numbers::pi / 2.0, 0.5, + auto previous = arith::SubIOp::create(builder, index, one); + detail::phaseRotationLoop( + builder, zero, index, one, firstAngle, half, [&](Value angle, Value distance) { auto bit = arith::SubIOp::create(builder, previous, distance); builder.scfIf(result, bit, [&] { builder.p(angle, query); }); }); builder.h(query); - builder.measure(query, result, step); + builder.measure(query, result, index); builder.reset(query); }; diff --git a/mlir/bench/programs/QFTAdder.cpp b/mlir/bench/programs/QFTAdder.cpp new file mode 100644 index 0000000000..11b7f2b516 --- /dev/null +++ b/mlir/bench/programs/QFTAdder.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "bench/QFTAdder.hpp" + +#include "Programs.h" +#include "QFTUtils.h" +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mqt::bench { + +using namespace mlir; + +static void prepareRegister(qc::QCProgramBuilder& builder, Value reg, + std::string_view bits) { + for (size_t first = 0; first < bits.size();) { + auto last = bits.find_first_not_of(bits[first], first); + if (last == std::string_view::npos) { + last = bits.size(); + } + if (bits[first] != '0') { + builder.scfFor(static_cast(bits.size() - last), + static_cast(bits.size() - first), 1, + [&](Value index) { + auto qubit = builder.loadQubit(reg, index); + if (bits[first] == '+') { + builder.h(qubit); + } else { + builder.x(qubit); + } + }); + } + first = last; + } +} + +static void addQuantumRegister(qc::QCProgramBuilder& builder, Value addend, + Value sum, int64_t qubits, bool carry) { + auto zero = builder.indexConstant(0); + auto one = builder.indexConstant(1); + auto last = builder.indexConstant(qubits - 1); + auto firstAngle = builder.floatConstant(std::numbers::pi); + auto half = builder.floatConstant(0.5); + builder.scfFor(0, qubits, 1, [&](Value step) { + auto target = arith::SubIOp::create(builder, last, step).getResult(); + auto upper = arith::AddIOp::create(builder, target, one).getResult(); + detail::phaseRotationLoop( + builder, zero, upper, one, firstAngle, half, + [&](Value angle, Value distance) { + auto control = + arith::SubIOp::create(builder, target, distance).getResult(); + builder.cp(angle, builder.loadQubit(addend, control), + builder.loadQubit(sum, target)); + }); + }); + if (carry) { + auto target = builder.loadQubit(sum, builder.indexConstant(qubits)); + detail::phaseRotationLoop( + builder, zero, builder.indexConstant(qubits), one, + builder.floatConstant(std::numbers::pi / 2.), half, + [&](Value angle, Value distance) { + auto control = arith::SubIOp::create(builder, last, distance); + builder.cp(angle, builder.loadQubit(addend, control), target); + }); + } +} + +[[nodiscard]] static Value phaseAngles(qc::QCProgramBuilder& builder, + std::string_view addend, bool carry) { + SmallVector angles; + angles.reserve(addend.size() + 1U); + long double angle = 0.L; + for (const char bit : addend | std::views::reverse) { + angle /= 2.L; + if (bit == '1') { + angle += std::numbers::pi_v; + } + angles.push_back(static_cast(angle)); + } + if (carry) { + angles.push_back(static_cast(angle / 2.L)); + } + + const auto type = RankedTensorType::get({static_cast(angles.size())}, + builder.getF64Type()); + const auto value = DenseElementsAttr::get(type, ArrayRef(angles)); + return arith::ConstantOp::create(builder, value).getResult(); +} + +SmallVector qftAdder(qc::QCProgramBuilder& builder, + const QFTAdder& benchmark) { + const auto& options = benchmark.options(); + const auto qubits = static_cast(options.addend.size()); + const auto carry = options.overflow == QFTAdderOverflow::Carry; + const auto sumQubits = qubits + static_cast(carry); + Value addend; + if (options.method == QFTAdderMethod::Register) { + addend = builder.allocQubitRegisterStorage(qubits, "addend"); + prepareRegister(builder, addend, options.addend); + } + auto sum = builder.allocQubitRegisterStorage(sumQubits, "sum"); + prepareRegister(builder, sum, options.accumulator); + auto result = builder.allocClassicalBitRegister( + static_cast(benchmark.output().width), benchmark.output().name); + + detail::forwardQFT(builder, sum, sumQubits); + if (addend) { + addQuantumRegister(builder, addend, sum, qubits, carry); + } else { + auto angles = phaseAngles(builder, options.addend, carry); + builder.scfFor(0, sumQubits, 1, [&](Value target) { + auto angle = + tensor::ExtractOp::create(builder, angles, ValueRange{target}); + builder.p(angle, builder.loadQubit(sum, target)); + }); + } + detail::inverseQFT(builder, sum, sumQubits); + + builder.measureQubitRegister(sum, result, sumQubits); + if (addend) { + auto offset = builder.indexConstant(sumQubits); + builder.scfFor(0, qubits, 1, [&](Value index) { + auto resultIndex = arith::AddIOp::create(builder, offset, index); + builder.measure(builder.loadQubit(addend, index), result, resultIndex); + }); + } + return {result}; +} + +} // namespace mqt::bench diff --git a/mlir/bench/programs/QFTUtils.cpp b/mlir/bench/programs/QFTUtils.cpp new file mode 100644 index 0000000000..f034a82d34 --- /dev/null +++ b/mlir/bench/programs/QFTUtils.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "QFTUtils.h" + +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace mqt::bench::detail { + +using namespace mlir; + +void phaseRotationLoop( + qc::QCProgramBuilder& builder, Value lower, Value upper, Value step, + Value initialAngle, Value scale, + const function_ref& body) { + auto loop = + scf::ForOp::create(builder, lower, upper, step, ValueRange{initialAngle}); + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(loop.getBody()); + auto angle = loop.getRegionIterArg(0); + body(angle, loop.getInductionVar()); + auto next = arith::MulFOp::create(builder, angle, scale).getResult(); + scf::YieldOp::create(builder, ValueRange{next}); +} + +void forwardQFT(qc::QCProgramBuilder& builder, Value qubitRegister, + int64_t qubits) { + auto zero = builder.indexConstant(0); + auto one = builder.indexConstant(1); + auto last = builder.indexConstant(qubits - 1); + auto firstAngle = builder.floatConstant(std::numbers::pi / 2.); + auto half = builder.floatConstant(0.5); + builder.scfFor(0, qubits, 1, [&](Value step) { + auto target = arith::SubIOp::create(builder, last, step).getResult(); + builder.h(builder.loadQubit(qubitRegister, target)); + + auto previous = arith::SubIOp::create(builder, target, one).getResult(); + phaseRotationLoop( + builder, zero, target, one, firstAngle, half, + [&](Value angle, Value distance) { + auto control = + arith::SubIOp::create(builder, previous, distance).getResult(); + builder.cp(angle, builder.loadQubit(qubitRegister, control), + builder.loadQubit(qubitRegister, target)); + }); + }); +} + +void inverseQFT(qc::QCProgramBuilder& builder, Value qubitRegister, + int64_t qubits) { + auto zero = builder.indexConstant(0); + auto one = builder.indexConstant(1); + auto upper = builder.indexConstant(qubits); + auto firstAngle = builder.floatConstant(-std::numbers::pi / 2.); + auto half = builder.floatConstant(0.5); + builder.scfFor(zero, upper, 1, [&](Value target) { + auto previous = arith::SubIOp::create(builder, target, one).getResult(); + phaseRotationLoop( + builder, zero, target, one, firstAngle, half, + [&](Value angle, Value distance) { + auto control = + arith::SubIOp::create(builder, previous, distance).getResult(); + builder.cp(angle, builder.loadQubit(qubitRegister, control), + builder.loadQubit(qubitRegister, target)); + }); + builder.h(builder.loadQubit(qubitRegister, target)); + }); +} + +} // namespace mqt::bench::detail diff --git a/mlir/bench/programs/QFTUtils.h b/mlir/bench/programs/QFTUtils.h new file mode 100644 index 0000000000..e9d26e2eb0 --- /dev/null +++ b/mlir/bench/programs/QFTUtils.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include + +#include + +namespace mlir { +class Value; + +namespace qc { +class QCProgramBuilder; +} // namespace qc +} // namespace mlir + +namespace mqt::bench::detail { + +/// Emit a loop whose phase angle follows a geometric sequence. +void phaseRotationLoop( + mlir::qc::QCProgramBuilder& builder, mlir::Value lower, mlir::Value upper, + mlir::Value step, mlir::Value initialAngle, mlir::Value scale, + const mlir::function_ref& body); + +/// Apply the exact no-swap QFT. +void forwardQFT(mlir::qc::QCProgramBuilder& builder, mlir::Value qubitRegister, + int64_t qubits); + +/// Apply the exact inverse of `forwardQFT`. +void inverseQFT(mlir::qc::QCProgramBuilder& builder, mlir::Value qubitRegister, + int64_t qubits); + +} // namespace mqt::bench::detail diff --git a/mlir/bench/programs/QPE.cpp b/mlir/bench/programs/QPE.cpp index b18a010435..78993f226e 100644 --- a/mlir/bench/programs/QPE.cpp +++ b/mlir/bench/programs/QPE.cpp @@ -11,13 +11,12 @@ #include "bench/QPE.hpp" #include "Programs.h" +#include "QFTUtils.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include #include -#include #include -#include #include #include #include @@ -33,22 +32,6 @@ namespace mqt::bench { using namespace mlir; -static void -qpePhaseRotationLoop(qc::QCProgramBuilder& builder, Value lower, Value upper, - const double start, const double factor, - const function_ref& body) { - auto one = builder.indexConstant(1); - auto first = builder.floatConstant(start); - auto scale = builder.floatConstant(factor); - auto loop = scf::ForOp::create(builder, lower, upper, one, ValueRange{first}); - OpBuilder::InsertionGuard guard(builder); - builder.setInsertionPointToStart(loop.getBody()); - auto angle = loop.getRegionIterArg(0); - body(angle, loop.getInductionVar()); - auto next = arith::MulFOp::create(builder, angle, scale).getResult(); - scf::YieldOp::create(builder, ValueRange{next}); -} - [[nodiscard]] static Value controlledPhaseAngles(qc::QCProgramBuilder& builder, const QPE& benchmark) { const auto& options = benchmark.options(); @@ -89,24 +72,26 @@ iterativeQPE(qc::QCProgramBuilder& builder, const QPE& benchmark) { auto one = builder.indexConstant(1); auto last = builder.indexConstant(precision - 1); auto angles = controlledPhaseAngles(builder, benchmark); + auto firstCorrection = builder.floatConstant(-std::numbers::pi / 2.); + auto half = builder.floatConstant(0.5); - builder.scfFor(lower, upper, 1, [&](Value step) { - auto power = arith::SubIOp::create(builder, last, step); + builder.scfFor(lower, upper, 1, [&](Value index) { + auto power = arith::SubIOp::create(builder, last, index); auto angle = tensor::ExtractOp::create(builder, angles, ValueRange{power}) .getResult(); builder.h(query); builder.cp(angle, query, ancilla); - auto previous = arith::SubIOp::create(builder, step, one); - qpePhaseRotationLoop( - builder, lower, step, -std::numbers::pi / 2.0, 0.5, + auto previous = arith::SubIOp::create(builder, index, one); + detail::phaseRotationLoop( + builder, lower, index, one, firstCorrection, half, [&](Value correction, Value distance) { auto bit = arith::SubIOp::create(builder, previous, distance); builder.scfIf(result, bit, [&] { builder.p(correction, query); }); }); builder.h(query); - builder.measure(query, result, step); + builder.measure(query, result, index); builder.reset(query); }); return {result}; @@ -126,7 +111,6 @@ standardQPE(qc::QCProgramBuilder& builder, const QPE& benchmark) { auto zero = builder.indexConstant(0); auto upper = builder.indexConstant(precision); - auto one = builder.indexConstant(1); auto last = builder.indexConstant(precision - 1); auto angles = controlledPhaseAngles(builder, benchmark); builder.scfFor(zero, upper, 1, [&](Value index) { @@ -136,17 +120,7 @@ standardQPE(qc::QCProgramBuilder& builder, const QPE& benchmark) { builder.cp(angle, builder.loadQubit(query, control), ancilla); }); - builder.scfFor(zero, upper, 1, [&](Value step) { - auto previous = arith::SubIOp::create(builder, step, one); - qpePhaseRotationLoop(builder, zero, step, -std::numbers::pi / 2.0, 0.5, - [&](Value angle, Value distance) { - auto control = arith::SubIOp::create( - builder, previous, distance); - builder.cp(angle, builder.loadQubit(query, control), - builder.loadQubit(query, step)); - }); - builder.h(builder.loadQubit(query, step)); - }); + detail::inverseQFT(builder, query, precision); builder.measureQubitRegister(query, result, precision); return {result}; } diff --git a/mlir/include/mlir/bench/Generate.h b/mlir/include/mlir/bench/Generate.h index a7ac604611..f8c865fcff 100644 --- a/mlir/include/mlir/bench/Generate.h +++ b/mlir/include/mlir/bench/Generate.h @@ -22,6 +22,7 @@ class GHZ; class Grover; class Multiplexer; class QFT; +class QFTAdder; class QPE; class Teleportation; @@ -49,6 +50,10 @@ generate(const Multiplexer& benchmark); /// Generate a configured quantum Fourier-transform benchmark. [[nodiscard]] std::optional generate(const QFT& benchmark); +/// Generate a configured QFT adder benchmark. +[[nodiscard]] std::optional +generate(const QFTAdder& benchmark); + /// Generate the QC program for a configured QPE benchmark. [[nodiscard]] std::optional generate(const QPE& benchmark); diff --git a/mlir/unittests/bench/CMakeLists.txt b/mlir/unittests/bench/CMakeLists.txt index 333ffd40da..379c1c7978 100644 --- a/mlir/unittests/bench/CMakeLists.txt +++ b/mlir/unittests/bench/CMakeLists.txt @@ -14,13 +14,14 @@ add_executable( test_benchmark_generate_grover.cpp test_benchmark_generate_multiplexer.cpp test_benchmark_generate_qft.cpp + test_benchmark_generate_qft_adder.cpp test_benchmark_generate_qpe.cpp test_benchmark_generate_teleportation.cpp) target_link_libraries(mqt-core-mlir-unittests-benchmark PRIVATE GTest::gtest_main MQT::CoreBenchGenerate MLIRQCODDFunctionality) -mqt_mlir_configure_unittest_target(mqt-core-mlir-unittests-benchmark) +mqt_mlir_configure_unittest_target(mqt-core-mlir-unittests-benchmark REQUIRES_EH) gtest_discover_tests(mqt-core-mlir-unittests-benchmark PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/bench/TestUtils.h b/mlir/unittests/bench/TestUtils.h index 0dc86d4e12..0aafa1dfe8 100644 --- a/mlir/unittests/bench/TestUtils.h +++ b/mlir/unittests/bench/TestUtils.h @@ -16,8 +16,11 @@ #include "mlir/bench/Generate.h" #include +#include +#include #include #include +#include #include #include @@ -57,6 +60,18 @@ void expectSamplingMatchesReference(const Benchmark& benchmark) { EXPECT_LT(benchmark.evaluate(*counts).totalVariationDistance, 0.03); } +[[nodiscard]] inline mlir::DenseElementsAttr +angleTable(mlir::ModuleOp moduleOp) { + mlir::DenseElementsAttr result; + moduleOp.walk([&](mlir::arith::ConstantOp op) { + if (auto table = mlir::dyn_cast(op.getValue())) { + EXPECT_FALSE(result); + result = table; + } + }); + return result; +} + template [[nodiscard]] size_t countOps(mlir::ModuleOp moduleOp) { size_t count = 0; moduleOp.walk([&count](Op /*unused*/) { ++count; }); diff --git a/mlir/unittests/bench/test_benchmark_cli.cmake b/mlir/unittests/bench/test_benchmark_cli.cmake index 6f0494537e..888b5e2c5d 100644 --- a/mlir/unittests/bench/test_benchmark_cli.cmake +++ b/mlir/unittests/bench/test_benchmark_cli.cmake @@ -45,8 +45,8 @@ endif() run_success("benchmark listing" list_output "${CLI}" list) string(JSON benchmark_count LENGTH "${list_output}" benchmarks) -if(NOT benchmark_count EQUAL 7) - message(FATAL_ERROR "list returned ${benchmark_count} benchmarks instead of 7") +if(NOT benchmark_count EQUAL 8) + message(FATAL_ERROR "list returned ${benchmark_count} benchmarks instead of 8") endif() run_success("multiplexer description" describe_output "${CLI}" describe multiplexer) diff --git a/mlir/unittests/bench/test_benchmark_generate.cpp b/mlir/unittests/bench/test_benchmark_generate.cpp index 6d63037936..a6199f743a 100644 --- a/mlir/unittests/bench/test_benchmark_generate.cpp +++ b/mlir/unittests/bench/test_benchmark_generate.cpp @@ -14,6 +14,7 @@ #include "bench/Grover.hpp" #include "bench/Multiplexer.hpp" #include "bench/QFT.hpp" +#include "bench/QFTAdder.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" #include "mlir/bench/Generate.h" @@ -44,6 +45,13 @@ TEST(GenerateProgramTest, GeneratesEveryBenchmarkMethodAsQCAndJeff) { expectValidQCAndJeff(QFT{{.qubits = 3, .periodExponent = 1}}); expectValidQCAndJeff(QFT{ {.qubits = 3, .periodExponent = 1, .method = QFTMethod::Semiclassical}}); + expectValidQCAndJeff(QFTAdder{{ + .addend = "101", + .accumulator = "001", + .method = QFTAdderMethod::Constant, + .overflow = QFTAdderOverflow::Carry, + }}); + expectValidQCAndJeff(QFTAdder{{.addend = "+++", .accumulator = "001"}}); expectValidQCAndJeff(QPE{{.precision = 3, .phase = Phase(3, 8)}}); expectValidQCAndJeff(QPE{ {.precision = 3, .phase = Phase(3, 8), .method = QPEMethod::Iterative}}); diff --git a/mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp new file mode 100644 index 0000000000..479bee586f --- /dev/null +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "TestUtils.h" +#include "bench/Evaluation.hpp" +#include "bench/JSON.hpp" +#include "bench/QFTAdder.hpp" +#include "dd/DDDefinitions.hpp" +#include "dd/Package.hpp" +#include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/bench/Generate.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::bench { + +using namespace mlir; + +static void expectConstantIndex(Value value, int64_t expected) { + auto constant = value.getDefiningOp(); + ASSERT_TRUE(constant); + EXPECT_EQ(constant.value(), expected); +} + +static void expectConstantFloat(Value value, double expected) { + auto constant = value.getDefiningOp(); + ASSERT_TRUE(constant); + auto attribute = dyn_cast(constant.getValue()); + ASSERT_TRUE(attribute); + EXPECT_DOUBLE_EQ(attribute.getValueAsDouble(), expected); +} + +TEST(GenerateProgramTest, EmitsQuantumQFTAdderCircuit) { + constexpr int64_t qubits = 3; + auto program = generate(QFTAdder{{.addend = "+++", .accumulator = "001"}}); + ASSERT_TRUE(program); + auto moduleOp = program->module(); + + EXPECT_EQ(test::countOps(moduleOp), 2U); + EXPECT_EQ(test::countOps(moduleOp), 1U); + EXPECT_EQ(test::countOps(moduleOp), 3U); + EXPECT_EQ(test::countOps(moduleOp), 1U); + EXPECT_EQ(test::countOps(moduleOp), 3U); + EXPECT_EQ(test::countOps(moduleOp), 3U); + EXPECT_EQ(test::countOps(moduleOp), 2U); + EXPECT_EQ(test::countOps(moduleOp), 0U); + + /// Unlike the QFT phases, the addition phase connects the two registers. + qc::CtrlOp addition; + moduleOp.walk([&](qc::CtrlOp op) { + auto control = op.getControl(0).getDefiningOp(); + auto target = op.getTarget(0).getDefiningOp(); + if (control && target && control.getMemref() != target.getMemref()) { + EXPECT_FALSE(addition); + addition = op; + } + }); + ASSERT_TRUE(addition); + + auto sourceLoad = addition.getControl(0).getDefiningOp(); + auto targetLoad = addition.getTarget(0).getDefiningOp(); + ASSERT_TRUE(sourceLoad); + ASSERT_TRUE(targetLoad); + + auto inner = addition->getParentOfType(); + ASSERT_TRUE(inner); + auto outer = inner->getParentOfType(); + ASSERT_TRUE(outer); + + auto target = targetLoad.getIndices().front(); + auto targetIndex = target.getDefiningOp(); + ASSERT_TRUE(targetIndex); + expectConstantIndex(targetIndex.getLhs(), qubits - 1); + EXPECT_EQ(targetIndex.getRhs(), outer.getInductionVar()); + + auto sourceIndex = + sourceLoad.getIndices().front().getDefiningOp(); + ASSERT_TRUE(sourceIndex); + EXPECT_EQ(sourceIndex.getLhs(), target); + EXPECT_EQ(sourceIndex.getRhs(), inner.getInductionVar()); + + auto upper = inner.getUpperBound().getDefiningOp(); + ASSERT_TRUE(upper); + expectConstantIndex(upper.getLhs(), qubits); + EXPECT_EQ(upper.getRhs(), outer.getInductionVar()); + expectConstantIndex(inner.getLowerBound(), 0); + expectConstantIndex(inner.getStep(), 1); + + ASSERT_EQ(inner.getInitArgs().size(), 1U); + expectConstantFloat(inner.getInitArgs().front(), std::numbers::pi); + qc::POp phase; + addition.walk([&](qc::POp op) { phase = op; }); + ASSERT_TRUE(phase); + EXPECT_EQ(phase.getTheta(), inner.getRegionIterArg(0)); + + auto yield = dyn_cast(inner.getBody()->getTerminator()); + ASSERT_TRUE(yield); + ASSERT_EQ(yield.getNumOperands(), 1U); + auto nextAngle = yield.getOperand(0).getDefiningOp(); + ASSERT_TRUE(nextAngle); + EXPECT_EQ(nextAngle.getLhs(), inner.getRegionIterArg(0)); + expectConstantFloat(nextAngle.getRhs(), 0.5); +} + +TEST(GenerateProgramTest, KeepsLargestQuantumQFTAdderFiniteAndStructured) { + auto program = generate(QFTAdder{{ + .addend = std::string(QFTAdderOptions::MAX_QUBITS, '+'), + .accumulator = std::string(QFTAdderOptions::MAX_QUBITS - 1, '0') + "1", + }}); + ASSERT_TRUE(program); + auto moduleOp = program->module(); + + EXPECT_LT(test::countOperations(moduleOp), 200U); + moduleOp.walk([&](arith::ConstantOp op) { + if (auto value = dyn_cast(op.getValue())) { + EXPECT_TRUE(std::isfinite(value.getValueAsDouble())); + } + }); +} + +static void expectPhaseLoopConstantIndex(Value value, int64_t expected) { + auto constant = value.getDefiningOp(); + ASSERT_TRUE(constant); + EXPECT_EQ(constant.value(), expected); +} + +TEST(GenerateProgramTest, UsesConfiguredClassicalQFTAdderPhases) { + auto program = generate(QFTAdder{{ + .addend = "101", + .accumulator = "001", + .method = QFTAdderMethod::Constant, + .overflow = QFTAdderOverflow::Carry, + }}); + ASSERT_TRUE(program); + auto moduleOp = program->module(); + + auto table = test::angleTable(moduleOp); + ASSERT_TRUE(table); + const auto angles = llvm::to_vector(table.getValues()); + ASSERT_EQ(angles.size(), 4U); + EXPECT_DOUBLE_EQ(angles[0], std::numbers::pi); + EXPECT_DOUBLE_EQ(angles[1], std::numbers::pi / 2.); + EXPECT_DOUBLE_EQ(angles[2], 5. * std::numbers::pi / 4.); + EXPECT_DOUBLE_EQ(angles[3], 5. * std::numbers::pi / 8.); + + tensor::ExtractOp extract; + moduleOp.walk([&](tensor::ExtractOp op) { + EXPECT_FALSE(extract); + extract = op; + }); + ASSERT_TRUE(extract); + auto loop = extract->getParentOfType(); + ASSERT_TRUE(loop); + expectPhaseLoopConstantIndex(loop.getLowerBound(), 0); + expectPhaseLoopConstantIndex(loop.getUpperBound(), 4); + expectPhaseLoopConstantIndex(loop.getStep(), 1); + EXPECT_EQ(extract.getIndices().front(), loop.getInductionVar()); + + qc::POp phase; + moduleOp.walk([&](qc::POp op) { + if (!op->getParentOfType()) { + EXPECT_FALSE(phase); + phase = op; + } + }); + ASSERT_TRUE(phase); + EXPECT_EQ(phase->getParentOfType(), loop); + EXPECT_EQ(phase.getTheta(), extract.getResult()); + auto target = phase.getQubit(0).getDefiningOp(); + ASSERT_TRUE(target); + EXPECT_EQ(target.getIndices().front(), loop.getInductionVar()); +} + +TEST(GenerateProgramTest, KeepsLargestClassicalQFTAdderFiniteAndStructured) { + auto addend = std::string((QFTAdderOptions::MAX_QUBITS - 1U), '1'); + auto program = generate(QFTAdder{{ + .addend = std::move(addend), + .accumulator = std::string(QFTAdderOptions::MAX_QUBITS - 2, '0') + "1", + .method = QFTAdderMethod::Constant, + .overflow = QFTAdderOverflow::Carry, + }}); + ASSERT_TRUE(program); + auto moduleOp = program->module(); + + auto table = test::angleTable(moduleOp); + ASSERT_TRUE(table); + EXPECT_EQ(table.getNumElements(), (QFTAdderOptions::MAX_QUBITS - 1U) + 1U); + for (const auto angle : table.getValues()) { + EXPECT_TRUE(std::isfinite(angle)); + } + + EXPECT_EQ(test::countOps(moduleOp), 1U); + EXPECT_LT(test::countOperations(moduleOp), 100U); +} + +TEST(GenerateProgramTest, SamplesEverySmallQFTAdderOperandPair) { + for (const auto method : + {QFTAdderMethod::Register, QFTAdderMethod::Constant}) { + for (const auto overflow : + {QFTAdderOverflow::Wrap, QFTAdderOverflow::Carry}) { + for (size_t width = 1; width <= 3; ++width) { + const auto sumWidth = + width + (overflow == QFTAdderOverflow::Carry ? 1U : 0U); + for (size_t addend = 0; addend < (size_t{1} << width); ++addend) { + for (size_t accumulator = 0; accumulator < (size_t{1} << width); + ++accumulator) { + const auto addendBits = dd::intToBinaryString(addend, width); + const QFTAdder benchmark{{ + .addend = addendBits, + .accumulator = dd::intToBinaryString(accumulator, width), + .method = method, + .overflow = overflow, + }}; + SCOPED_TRACE(toInstanceSpecificationJSON(benchmark)); + const auto total = (addend + accumulator) % (size_t{1} << sumWidth); + const auto expected = + (method == QFTAdderMethod::Register ? addendBits : "") + + dd::intToBinaryString(total, sumWidth); + EXPECT_EQ(benchmark.expectedResult(), expected); + auto program = test::generateQCO(benchmark); + ASSERT_TRUE(program); + auto counts = qco::sample( + mlir::mqt::getEntryPoint(program->module()), 32, 17); + ASSERT_TRUE(succeeded(counts)); + EXPECT_EQ(*counts, (Counts{{expected, 32}})); + } + } + } + } + } +} + +TEST(GenerateProgramTest, PreservesQFTAdderRelativePhases) { + for (const auto overflow : + {QFTAdderOverflow::Wrap, QFTAdderOverflow::Carry}) { + for (size_t width = 1; width <= 3; ++width) { + const auto sumWidth = + width + (overflow == QFTAdderOverflow::Carry ? 1U : 0U); + for (size_t accumulator = 0; accumulator < (size_t{1} << width); + ++accumulator) { + const QFTAdder benchmark{{ + .addend = std::string(width, '+'), + .accumulator = dd::intToBinaryString(accumulator, width), + .overflow = overflow, + }}; + SCOPED_TRACE(toInstanceSpecificationJSON(benchmark)); + auto program = test::generateQCO(benchmark); + ASSERT_TRUE(program); + dd::Package package(0); + auto state = qco::simulateStatevector( + mlir::mqt::getEntryPoint(program->module()), package); + ASSERT_TRUE(succeeded(state)); + const auto actual = state->getVector(); + package.decRef(*state); + dd::CVec expected(size_t{1} << (width + sumWidth)); + for (size_t addend = 0; addend < (size_t{1} << width); ++addend) { + const auto total = (addend + accumulator) % (size_t{1} << sumWidth); + expected[(total << width) | addend] = + 1. / std::sqrt(static_cast(size_t{1} << width)); + } + ASSERT_EQ(actual.size(), expected.size()); + const auto overlap = + std::inner_product(expected.begin(), expected.end(), actual.begin(), + std::complex{}, std::plus<>(), + [](const auto& lhs, const auto& rhs) { + return std::conj(lhs) * rhs; + }); + const auto phase = std::polar(1., std::arg(overlap)); + for (size_t i = 0; i < actual.size(); ++i) { + EXPECT_NEAR(std::abs(actual[i] - phase * expected[i]), 0., 1e-12) + << i; + } + } + } + } +} + +TEST(GenerateProgramTest, SamplesPartlySuperposedQFTAdder) { + test::expectSamplingMatchesReference( + QFTAdder{{.addend = "1+0", .accumulator = "001"}}); +} + +} // namespace mqt::bench diff --git a/mlir/unittests/bench/test_benchmark_generate_qpe.cpp b/mlir/unittests/bench/test_benchmark_generate_qpe.cpp index e30460530b..695253ec93 100644 --- a/mlir/unittests/bench/test_benchmark_generate_qpe.cpp +++ b/mlir/unittests/bench/test_benchmark_generate_qpe.cpp @@ -34,17 +34,6 @@ namespace mqt::bench { using namespace mlir; -[[nodiscard]] static DenseElementsAttr angleTable(ModuleOp moduleOp) { - DenseElementsAttr result; - moduleOp.walk([&](arith::ConstantOp op) { - if (const auto table = dyn_cast(op.getValue())) { - EXPECT_FALSE(result); - result = table; - } - }); - return result; -} - TEST(GenerateProgramTest, KeepsStandardQPEPowerAndResultOrderAligned) { const QPE benchmark({.precision = 2, .phase = Phase(1, 4)}); EXPECT_DOUBLE_EQ(benchmark.probability("01"), 1.); @@ -52,7 +41,7 @@ TEST(GenerateProgramTest, KeepsStandardQPEPowerAndResultOrderAligned) { auto program = generate(benchmark); ASSERT_TRUE(program); auto moduleOp = program->module(); - auto table = angleTable(moduleOp); + auto table = test::angleTable(moduleOp); ASSERT_TRUE(table); const auto angles = llvm::to_vector(table.getValues()); ASSERT_EQ(angles.size(), 2U); @@ -109,7 +98,7 @@ TEST(GenerateProgramTest, KeepsLargeQPEFiniteAndStructured) { ASSERT_TRUE(program); auto moduleOp = program->module(); - auto table = angleTable(moduleOp); + auto table = test::angleTable(moduleOp); ASSERT_TRUE(table); EXPECT_EQ(table.getNumElements(), precision); for (const auto angle : table.getValues()) { @@ -128,7 +117,7 @@ TEST(GenerateProgramTest, DoublesQPEPhaseModuloOneWithoutOverflow) { }); auto program = generate(benchmark); ASSERT_TRUE(program); - const auto table = angleTable(program->module()); + const auto table = test::angleTable(program->module()); ASSERT_TRUE(table); const auto angles = llvm::to_vector(table.getValues()); ASSERT_EQ(angles.size(), 4U); diff --git a/python/mqt/core/bench/__init__.pyi b/python/mqt/core/bench/__init__.pyi index 80101754bd..dbf805b779 100644 --- a/python/mqt/core/bench/__init__.pyi +++ b/python/mqt/core/bench/__init__.pyi @@ -13,6 +13,7 @@ from mqt.core.bench import ghz as ghz from mqt.core.bench import grover as grover from mqt.core.bench import multiplexer as multiplexer from mqt.core.bench import qft as qft +from mqt.core.bench import qft_adder as qft_adder from mqt.core.bench import qpe as qpe from mqt.core.bench import teleportation as teleportation diff --git a/python/mqt/core/bench/qft_adder.pyi b/python/mqt/core/bench/qft_adder.pyi new file mode 100644 index 0000000000..63178b95e6 --- /dev/null +++ b/python/mqt/core/bench/qft_adder.pyi @@ -0,0 +1,106 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""QFT adder benchmark instances and options.""" + +import enum +from collections.abc import Mapping + +import mqt.core.bench +import mqt.core.mlir + +class Method(enum.Enum): + """How the addend enters the circuit.""" + + REGISTER = 0 + + CONSTANT = 1 + +class Overflow(enum.Enum): + """Wrap the sum or retain carry.""" + + WRAP = 0 + + CARRY = 1 + +class Options: + """Parameters for a QFT adder.""" + + def __init__( + self, *, addend: str, accumulator: str, method: Method = Method.REGISTER, overflow: Overflow = Overflow.WRAP + ) -> None: ... + @property + def addend(self) -> str: + """Big-endian addend; register inputs also accept ``+`` for a :math:`|+\\rangle` qubit.""" + + @property + def accumulator(self) -> str: + """Binary accumulator with the same width as the addend.""" + + @property + def method(self) -> Method: + """Register or constant addition.""" + + @property + def overflow(self) -> Overflow: + """Wrap or carry behavior.""" + +class QFTAdder: + """Add equal-width operands with an exact no-swap QFT circuit. + + Register addition uses controlled phases and returns the addend followed by the + sum. Constant addition compiles the addend into phases and returns only the sum. + Wrap mode computes :math:`(a + b) \\bmod 2^n`; carry mode retains one extra sum bit. + All strings are big-endian, and leading zeros determine the operand width. + + Register addends may contain ``+`` for independent :math:`|+\\rangle` qubits. + The accumulator and constant addends must be binary. The circuit follows + Draper's register addition and its constant-input Fourier specialization. + """ + + def __init__(self, options: Options) -> None: ... + @property + def options(self) -> Options: + """The resolved benchmark parameters.""" + + @property + def output(self) -> mqt.core.bench.Output: + """The logical output register.""" + + @property + def expected_result(self) -> str | None: + """The unique logical outcome, or None for a superposed addend.""" + + def probability(self, outcome: str) -> float: + """Return the ideal probability of an outcome.""" + + def evaluate(self, counts: Mapping[str, int]) -> mqt.core.bench.Evaluation: + """Compare sampled counts with the ideal distribution.""" + + def generate(self) -> mqt.core.mlir.QCProgram: + """Generate the benchmark as a QC program.""" + + @property + def instance_specification_json(self) -> str: + """The canonical instance specification JSON.""" + + @property + def manifest_json(self) -> str: + """The canonical manifest JSON.""" + + @property + def case_id(self) -> str: + """The stable semantic case ID.""" + + @staticmethod + def from_instance_specification_json(json: str, *, source: str = "") -> QFTAdder: + """Parse a strict benchmark instance specification.""" + + @staticmethod + def from_manifest_json(json: str, *, source: str = "") -> QFTAdder: + """Parse a strict benchmark manifest.""" diff --git a/src/bench/JSON.cpp b/src/bench/JSON.cpp index c06fe1b495..6f02a31f87 100644 --- a/src/bench/JSON.cpp +++ b/src/bench/JSON.cpp @@ -17,6 +17,7 @@ #include "bench/Grover.hpp" #include "bench/Multiplexer.hpp" #include "bench/QFT.hpp" +#include "bench/QFTAdder.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -423,6 +424,45 @@ parseMultiplexerParameters(const Json& parameters, } } +[[nodiscard]] QFTAdder parseQFTAdderParameters(const Json& parameters, + const std::string_view source) { + rejectUnknownKeys(parameters, {"addend", "accumulator", "method", "overflow"}, + source, "$/parameters"); + QFTAdderOptions options{ + .addend = + stringValue(required(parameters, "addend", source, "$/parameters"), + source, "$/parameters/addend"), + .accumulator = stringValue( + required(parameters, "accumulator", source, "$/parameters"), source, + "$/parameters/accumulator"), + }; + if (const auto it = parameters.find("method"); it != parameters.end()) { + const auto value = stringValue(*it, source, "$/parameters/method"); + if (value == "register") { + options.method = QFTAdderMethod::Register; + } else if (value == "constant") { + options.method = QFTAdderMethod::Constant; + } else { + fail(source, "$/parameters/method", "must be 'register' or 'constant'"); + } + } + if (const auto it = parameters.find("overflow"); it != parameters.end()) { + const auto value = stringValue(*it, source, "$/parameters/overflow"); + if (value == "wrap") { + options.overflow = QFTAdderOverflow::Wrap; + } else if (value == "carry") { + options.overflow = QFTAdderOverflow::Carry; + } else { + fail(source, "$/parameters/overflow", "must be 'wrap' or 'carry'"); + } + } + try { + return QFTAdder(std::move(options)); + } catch (const std::invalid_argument& error) { + fail(source, "$/parameters", error.what()); + } +} + [[nodiscard]] QPE parseQPEParameters(const Json& parameters, const std::string_view source) { rejectUnknownKeys(parameters, {"precision", "phase", "method"}, source, @@ -527,6 +567,22 @@ parseTeleportationParameters(const Json& parameters, }; } +[[nodiscard]] Json parametersJSON(const QFTAdder& benchmark) { + const auto& options = benchmark.options(); + return { + {"addend", options.addend}, + {"accumulator", options.accumulator}, + { + "method", + options.method == QFTAdderMethod::Register ? "register" : "constant", + }, + { + "overflow", + options.overflow == QFTAdderOverflow::Wrap ? "wrap" : "carry", + }, + }; +} + [[nodiscard]] Json parametersJSON(const QPE& benchmark) { const auto& options = benchmark.options(); return { @@ -598,6 +654,20 @@ parseTeleportationParameters(const Json& parameters, }; } +[[nodiscard]] Json referenceJSON(const QFTAdder& benchmark) { + Json reference = { + {"kind", "analytic"}, + {"model", "qft_adder"}, + {"outcome_order", "big_endian"}, + {"output", benchmark.output().name}, + {"version", 1}, + }; + if (benchmark.expectedResult()) { + reference["success_outcome"] = *benchmark.expectedResult(); + } + return reference; +} + [[nodiscard]] Json referenceJSON(const QPE& benchmark) { return { {"kind", "analytic"}, @@ -876,6 +946,108 @@ template }); } +[[nodiscard]] Json qftAdderInstanceSpecificationSchema() { + return baseInstanceSpecificationSchema({ + {"additionalProperties", false}, + { + "properties", + { + { + "addend", + { + {"type", "string"}, + {"minLength", 1}, + {"maxLength", QFTAdderOptions::MAX_QUBITS}, + {"pattern", "^[01+]+$"}, + }, + }, + { + "accumulator", + { + {"type", "string"}, + {"minLength", 1}, + {"maxLength", QFTAdderOptions::MAX_QUBITS}, + {"pattern", "^[01]+$"}, + }, + }, + { + "method", + { + {"type", "string"}, + {"enum", {"register", "constant"}}, + {"default", "register"}, + }, + }, + { + "overflow", + { + {"type", "string"}, + {"enum", {"wrap", "carry"}}, + {"default", "wrap"}, + }, + }, + }, + }, + { + "allOf", + { + { + { + "if", + { + {"properties", {{"method", {{"const", "constant"}}}}}, + {"required", {"method"}}, + }, + }, + { + "then", + {{"properties", {{"addend", {{"pattern", "^[01]+$"}}}}}}, + }, + }, + { + { + "if", + { + {"properties", {{"overflow", {{"const", "carry"}}}}}, + {"required", {"overflow"}}, + }, + }, + { + "then", + { + { + "properties", + { + { + "addend", + { + { + "maxLength", + QFTAdderOptions::MAX_QUBITS - 1U, + }, + }, + }, + { + "accumulator", + { + { + "maxLength", + QFTAdderOptions::MAX_QUBITS - 1U, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + {"required", {"addend", "accumulator"}}, + {"type", "object"}, + }); +} + [[nodiscard]] Json qpeInstanceSpecificationSchema() { return baseInstanceSpecificationSchema({ {"additionalProperties", false}, diff --git a/src/bench/QFTAdder.cpp b/src/bench/QFTAdder.cpp new file mode 100644 index 0000000000..a1eaa0cc5f --- /dev/null +++ b/src/bench/QFTAdder.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "bench/QFTAdder.hpp" + +#include "EvaluationUtils.hpp" +#include "bench/Evaluation.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::bench { +namespace { + +[[nodiscard]] std::string sumBits(const std::string_view addend, + const std::string_view accumulator, + const QFTAdderOverflow overflow) { + const auto offset = overflow == QFTAdderOverflow::Carry ? 1U : 0U; + auto result = std::string(addend.size() + offset, '0'); + auto carry = 0; + for (size_t i = addend.size(); i > 0; --i) { + const auto sum = (addend[i - 1] - '0') + (accumulator[i - 1] - '0') + carry; + result[i - 1 + offset] = static_cast('0' + (sum % 2)); + carry = sum / 2; + } + if (offset != 0) { + result[0] = static_cast('0' + carry); + } + return result; +} + +} // namespace + +QFTAdder::QFTAdder(QFTAdderOptions options) + : options_(std::move(options)), output_{.name = "result", .width = 0} { + if (options_.method != QFTAdderMethod::Register && + options_.method != QFTAdderMethod::Constant) { + throw std::invalid_argument( + "QFT adder method must be register or constant"); + } + if (options_.overflow != QFTAdderOverflow::Wrap && + options_.overflow != QFTAdderOverflow::Carry) { + throw std::invalid_argument("QFT adder overflow must be wrap or carry"); + } + const auto width = options_.addend.size(); + const auto carry = options_.overflow == QFTAdderOverflow::Carry; + if (width == 0 || + width > QFTAdderOptions::MAX_QUBITS - static_cast(carry) || + options_.accumulator.size() != width) { + throw std::invalid_argument("QFT adder operands must have equal nonzero " + "width, with at most 1024 sum bits"); + } + const auto isRegister = options_.method == QFTAdderMethod::Register; + if (options_.addend.find_first_not_of(isRegister ? "01+" : "01") != + std::string::npos || + options_.accumulator.find_first_not_of("01") != std::string::npos) { + throw std::invalid_argument("QFT adder operands must be binary; only " + "register addends may contain '+'"); + } + output_.width = + width + static_cast(carry) + (isRegister ? width : 0U); + if (options_.addend.find('+') == std::string::npos) { + expectedResult_ = + (isRegister ? options_.addend : std::string{}) + + sumBits(options_.addend, options_.accumulator, options_.overflow); + } +} + +const QFTAdderOptions& QFTAdder::options() const noexcept { return options_; } + +const Output& QFTAdder::output() const noexcept { return output_; } + +const std::optional& QFTAdder::expectedResult() const noexcept { + return expectedResult_; +} + +double QFTAdder::probability(const std::string_view outcome) const { + detail::validateOutcome(outcome, output_.width); + if (expectedResult_) { + return outcome == *expectedResult_ ? 1. : 0.; + } + const auto addend = outcome.substr(0, options_.addend.size()); + for (size_t i = 0; i < addend.size(); ++i) { + if (options_.addend[i] != '+' && options_.addend[i] != addend[i]) { + return 0.; + } + } + if (outcome.substr(addend.size()) != + sumBits(addend, options_.accumulator, options_.overflow)) { + return 0.; + } + return std::ldexp( + 1., -static_cast(std::ranges::count(options_.addend, '+'))); +} + +Evaluation QFTAdder::evaluate(const Counts& counts) const { + return detail::evaluate( + output_, counts, + [this](const std::string_view outcome) { return probability(outcome); }, + expectedResult_ ? std::optional(*expectedResult_) + : std::nullopt); +} + +} // namespace mqt::bench diff --git a/test/bench/test_json.cpp b/test/bench/test_json.cpp index ca3a4d6f9a..b5eb435735 100644 --- a/test/bench/test_json.cpp +++ b/test/bench/test_json.cpp @@ -15,6 +15,7 @@ #include "bench/JSON.hpp" #include "bench/Multiplexer.hpp" #include "bench/QFT.hpp" +#include "bench/QFTAdder.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -56,6 +57,11 @@ using mqt::bench::multiplexerFromInstanceSpecificationJSON; using mqt::bench::multiplexerFromManifestJSON; using mqt::bench::Phase; using mqt::bench::QFT; +using mqt::bench::QFTAdder; +using mqt::bench::qftAdderFromInstanceSpecificationJSON; +using mqt::bench::qftAdderFromManifestJSON; +using mqt::bench::QFTAdderMethod; +using mqt::bench::QFTAdderOverflow; using mqt::bench::qftFromInstanceSpecificationJSON; using mqt::bench::qftFromManifestJSON; using mqt::bench::QFTMethod; @@ -119,6 +125,14 @@ TEST(BenchmarkJSON, toInstanceSpecificationJSON(qft), R"({"benchmark":"qft","parameters":{"method":"standard","period_exponent":2,"qubits":4},"schema_version":1})"); + const auto qftAdder = qftAdderFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"qft-adder","parameters":{"addend":"+++","accumulator":"001"}})"); + EXPECT_EQ(qftAdder.options().method, QFTAdderMethod::Register); + EXPECT_EQ(qftAdder.options().overflow, QFTAdderOverflow::Wrap); + EXPECT_EQ( + toInstanceSpecificationJSON(qftAdder), + R"({"benchmark":"qft-adder","parameters":{"accumulator":"001","addend":"+++","method":"register","overflow":"wrap"},"schema_version":1})"); + const auto qpe = qpeFromInstanceSpecificationJSON( R"({"schema_version":1,"benchmark":"qpe","parameters":{"precision":4,"phase":{"numerator":10,"denominator":8},"method":"iterative"}})"); EXPECT_EQ(qpe.options().phase, Phase(1, 4)); @@ -142,6 +156,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { const Multiplexer multiplexer{{.qubits = 7}}; const QFT qft{ {.qubits = 4, .periodExponent = 2, .method = QFTMethod::Semiclassical}}; + const QFTAdder qftAdder{{.addend = "+++", .accumulator = "001"}}; const QPE qpe{ {.precision = 5, .phase = Phase(1, 3), .method = QPEMethod::Iterative}}; const Teleportation teleportation; @@ -151,6 +166,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { const auto groverManifest = toManifestJSON(grover); const auto multiplexerManifest = toManifestJSON(multiplexer); const auto qftManifest = toManifestJSON(qft); + const auto qftAdderManifest = toManifestJSON(qftAdder); const auto qpeManifest = toManifestJSON(qpe); const auto teleportationManifest = toManifestJSON(teleportation); EXPECT_EQ(toManifestJSON(bvFromManifestJSON(bvManifest)), bvManifest); @@ -160,6 +176,8 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { EXPECT_EQ(toManifestJSON(multiplexerFromManifestJSON(multiplexerManifest)), multiplexerManifest); EXPECT_EQ(toManifestJSON(qftFromManifestJSON(qftManifest)), qftManifest); + EXPECT_EQ(toManifestJSON(qftAdderFromManifestJSON(qftAdderManifest)), + qftAdderManifest); EXPECT_EQ(toManifestJSON(qpeFromManifestJSON(qpeManifest)), qpeManifest); EXPECT_EQ( toManifestJSON(teleportationFromManifestJSON(teleportationManifest)), @@ -169,6 +187,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { EXPECT_EQ(benchmarkIdFromManifestJSON(groverManifest), "grover"); EXPECT_EQ(benchmarkIdFromManifestJSON(multiplexerManifest), "multiplexer"); EXPECT_EQ(benchmarkIdFromManifestJSON(qftManifest), "qft"); + EXPECT_EQ(benchmarkIdFromManifestJSON(qftAdderManifest), "qft-adder"); EXPECT_EQ(benchmarkIdFromManifestJSON(qpeManifest), "qpe"); EXPECT_EQ(benchmarkIdFromManifestJSON(teleportationManifest), "teleportation"); @@ -178,6 +197,9 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { std::string::npos); EXPECT_NE(multiplexerManifest.find("\"model\":\"multiplexer\""), std::string::npos); + EXPECT_NE(qftAdderManifest.find("\"model\":\"qft_adder\""), + std::string::npos); + EXPECT_NE(qftAdderManifest.find("\"width\":6"), std::string::npos); EXPECT_EQ(qpeManifest.find("0.333"), std::string::npos); EXPECT_NE(teleportationManifest.find("\"model\":\"teleportation\""), std::string::npos); @@ -196,6 +218,10 @@ TEST(BenchmarkJSON, UsesStableSemanticCaseIds) { caseId(QFT{{.qubits = 3, .periodExponent = 1, .method = QFTMethod::Semiclassical}})); + EXPECT_EQ(caseId(QFTAdder{{.addend = "+++", .accumulator = "001"}}), + caseId(QFTAdder{{.addend = "+++", .accumulator = "001"}})); + EXPECT_NE(caseId(QFTAdder{{.addend = "+++", .accumulator = "001"}}), + caseId(QFTAdder{{.addend = "++++", .accumulator = "0001"}})); EXPECT_EQ(caseId(Multiplexer{{.qubits = 7}}), caseId(Multiplexer{{.qubits = 7}})); EXPECT_NE(caseId(Multiplexer{{.qubits = 7}}), @@ -275,6 +301,24 @@ TEST(BenchmarkJSON, R"({"schema_version":1,"benchmark":"multiplexer","parameters":{"qubits":7,"angles":[]}})")); }, "unknown key 'angles'"); + for (const auto* parameters : { + R"({"addend":"","accumulator":""})", + R"({"addend":"1","accumulator":"00"})", + R"({"addend":"+","accumulator":"0","method":"constant"})", + R"({"addend":"1","accumulator":"0","method":"unknown"})", + R"({"addend":"1","accumulator":"0","overflow":"unknown"})", + R"({"addend":"1","accumulator":"0","overflow":true})", + R"({"addend":"1","accumulator":"0","qubits":1})", + R"({"addend":"1"})", + }) { + const auto instance = + std::string{ + R"({"schema_version":1,"benchmark":"qft-adder","parameters":)"} + + parameters + "}"; + EXPECT_THROW( + static_cast(qftAdderFromInstanceSpecificationJSON(instance)), + std::invalid_argument); + } expectInvalid( [] { static_cast(teleportationFromInstanceSpecificationJSON( @@ -341,12 +385,13 @@ TEST(BenchmarkJSON, RejectsAlteredOrUnresolvedManifestData) { TEST(BenchmarkJSON, ListsBenchmarksAndDescribesStandardSchemas) { EXPECT_EQ( listBenchmarksJSON(), - R"({"benchmarks":[{"definition_version":1,"id":"bv"},{"definition_version":1,"id":"ghz"},{"definition_version":1,"id":"grover"},{"definition_version":1,"id":"multiplexer"},{"definition_version":1,"id":"qft"},{"definition_version":1,"id":"qpe"},{"definition_version":1,"id":"teleportation"}],"schema_version":1})"); + R"({"benchmarks":[{"definition_version":1,"id":"bv"},{"definition_version":1,"id":"ghz"},{"definition_version":1,"id":"grover"},{"definition_version":1,"id":"multiplexer"},{"definition_version":1,"id":"qft"},{"definition_version":1,"id":"qft-adder"},{"definition_version":1,"id":"qpe"},{"definition_version":1,"id":"teleportation"}],"schema_version":1})"); const auto bv = describeBenchmarkJSON("bv"); const auto ghz = describeBenchmarkJSON("ghz"); const auto grover = describeBenchmarkJSON("grover"); const auto multiplexer = describeBenchmarkJSON("multiplexer"); const auto qft = describeBenchmarkJSON("qft"); + const auto qftAdder = describeBenchmarkJSON("qft-adder"); const auto qpe = describeBenchmarkJSON("qpe"); const auto teleportation = describeBenchmarkJSON("teleportation"); EXPECT_NE(ghz.find("https://json-schema.org/draft/2020-12/schema"), @@ -359,6 +404,8 @@ TEST(BenchmarkJSON, ListsBenchmarksAndDescribesStandardSchemas) { EXPECT_NE(multiplexer.find("\"maximum\":1024"), std::string::npos); EXPECT_NE(multiplexer.find("\"minimum\":2"), std::string::npos); EXPECT_NE(qft.find("\"period_exponent\""), std::string::npos); + EXPECT_NE(qftAdder.find("\"maxLength\":1024"), std::string::npos); + EXPECT_NE(qftAdder.find("\"minLength\":1"), std::string::npos); EXPECT_NE(qpe.find("\"iterative\""), std::string::npos); EXPECT_NE( teleportation.find( @@ -397,6 +444,31 @@ TEST(BenchmarkJSON, ParsesCountsAndSerializesEvaluations) { EXPECT_NE(multiplexerEvaluation.find("\"total_variation_distance\":"), std::string::npos); + const QFTAdder qftAdder{{.addend = "++", .accumulator = "01"}}; + const auto qftAdderEvaluation = evaluateJSON( + toManifestJSON(qftAdder), + R"({"schema_version":1,"counts":{"0001":1,"0110":1,"1011":1,"1100":1}})"); + EXPECT_NE(qftAdderEvaluation.find("\"success_probability\":null"), + std::string::npos); + EXPECT_NE(qftAdderEvaluation.find("\"total_variation_distance\":0.0"), + std::string::npos); + + const QFTAdder constant{{ + .addend = "110", + .accumulator = "001", + .method = QFTAdderMethod::Constant, + .overflow = QFTAdderOverflow::Carry, + }}; + const auto constantEvaluation = + evaluateJSON(toManifestJSON(constant), + R"({"schema_version":1,"counts":{"0111":8,"0110":2}})"); + EXPECT_NE(constantEvaluation.find("\"success_probability\":0.8"), + std::string::npos); + EXPECT_EQ(toManifestJSON(qftAdderFromManifestJSON(toManifestJSON(constant))), + toManifestJSON(constant)); + EXPECT_NE(caseId(constant), + caseId(QFTAdder{{.addend = "110", .accumulator = "001"}})); + const Teleportation teleportation; const auto teleportationEvaluation = evaluateJSON(toManifestJSON(teleportation), diff --git a/test/bench/test_qft_adder.cpp b/test/bench/test_qft_adder.cpp new file mode 100644 index 0000000000..31c223816e --- /dev/null +++ b/test/bench/test_qft_adder.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "bench/QFTAdder.hpp" + +#include + +#include +#include + +namespace { + +using mqt::bench::QFTAdder; +using mqt::bench::QFTAdderMethod; +using mqt::bench::QFTAdderOptions; +using mqt::bench::QFTAdderOverflow; + +TEST(QFTAdder, PreservesConfiguredOperandsAndOverflow) { + for (const auto method : + {QFTAdderMethod::Register, QFTAdderMethod::Constant}) { + for (const auto overflow : + {QFTAdderOverflow::Wrap, QFTAdderOverflow::Carry}) { + const QFTAdder benchmark{{ + .addend = "011", + .accumulator = "110", + .method = method, + .overflow = overflow, + }}; + const auto* const sum = + overflow == QFTAdderOverflow::Carry ? "1001" : "001"; + const auto expected = + (method == QFTAdderMethod::Register ? std::string{"011"} + : std::string{}) + + sum; + EXPECT_EQ(benchmark.options().addend, "011"); + EXPECT_EQ(benchmark.options().accumulator, "110"); + EXPECT_EQ(benchmark.output().width, expected.size()); + EXPECT_EQ(benchmark.expectedResult(), expected); + EXPECT_DOUBLE_EQ(benchmark.probability(expected), 1.); + EXPECT_EQ(benchmark.evaluate({{expected, 16}}).successProbability, 1.); + } + } +} + +TEST(QFTAdder, KeepsLeadingZerosAndRejectsUnsupportedInputs) { + const QFTAdder zero{{ + .addend = "000", + .accumulator = "000", + .method = QFTAdderMethod::Constant, + .overflow = QFTAdderOverflow::Carry, + }}; + EXPECT_EQ(zero.expectedResult(), "0000"); + for (const auto& options : { + QFTAdderOptions{.addend = "", .accumulator = ""}, + QFTAdderOptions{.addend = "01", .accumulator = "1"}, + QFTAdderOptions{.addend = "x", .accumulator = "0"}, + QFTAdderOptions{.addend = "1", .accumulator = "+"}, + QFTAdderOptions{ + .addend = "+", + .accumulator = "0", + .method = QFTAdderMethod::Constant, + }, + }) { + EXPECT_THROW(static_cast(QFTAdder(options)), std::invalid_argument); + } + EXPECT_THROW(static_cast(zero.probability("000")), + std::invalid_argument); + EXPECT_THROW(static_cast(zero.probability("000x")), + std::invalid_argument); +} + +TEST(QFTAdder, ScoresTheCorrelatedSuperposition) { + const QFTAdder benchmark{{.addend = "1+0", .accumulator = "001"}}; + EXPECT_FALSE(benchmark.expectedResult()); + EXPECT_DOUBLE_EQ(benchmark.probability("100101"), 0.5); + EXPECT_DOUBLE_EQ(benchmark.probability("110111"), 0.5); + EXPECT_DOUBLE_EQ(benchmark.probability("000001"), 0.); + EXPECT_DOUBLE_EQ(benchmark.probability("100100"), 0.); + const auto exact = benchmark.evaluate({{"100101", 8}, {"110111", 8}}); + EXPECT_DOUBLE_EQ(exact.totalVariationDistance, 0.); + EXPECT_DOUBLE_EQ(exact.squaredHellingerFidelity, 1.); + EXPECT_FALSE(exact.successProbability); + const auto biased = benchmark.evaluate({{"100101", 16}}); + EXPECT_DOUBLE_EQ(biased.totalVariationDistance, 0.5); + EXPECT_DOUBLE_EQ(biased.squaredHellingerFidelity, 0.5); +} + +TEST(QFTAdder, BoundsTheSumWidthAndKeepsReferenceWeightsRepresentable) { + const auto width = QFTAdderOptions::MAX_QUBITS; + const auto accumulator = std::string(width - 1, '0') + "1"; + const QFTAdder maximum{ + {.addend = std::string(width, '+'), .accumulator = accumulator}}; + EXPECT_GT( + maximum.probability(std::string(width, '1') + std::string(width, '0')), + 0.); + EXPECT_THROW( + static_cast(QFTAdder({.addend = std::string(width, '1'), + .accumulator = accumulator, + .overflow = QFTAdderOverflow::Carry})), + std::invalid_argument); + EXPECT_NO_THROW( + static_cast(QFTAdder({.addend = std::string(width - 1, '1'), + .accumulator = accumulator.substr(1), + .overflow = QFTAdderOverflow::Carry}))); + EXPECT_THROW( + static_cast(QFTAdder({.addend = std::string(width + 1, '0'), + .accumulator = std::string(width + 1, '0')})), + std::invalid_argument); +} + +} // namespace diff --git a/test/python/bench/test_qft_adder.py b/test/python/bench/test_qft_adder.py new file mode 100644 index 0000000000..1981f51d26 --- /dev/null +++ b/test/python/bench/test_qft_adder.py @@ -0,0 +1,54 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Python bindings for the qft_adder benchmark.""" + +from __future__ import annotations + +import json + +import pytest + +from mqt.core.bench import qft_adder + +from .utils import assert_generates + + +@pytest.mark.parametrize("method", [qft_adder.Method.REGISTER, qft_adder.Method.CONSTANT]) +@pytest.mark.parametrize("overflow", [qft_adder.Overflow.WRAP, qft_adder.Overflow.CARRY]) +def test_qft_adder_reference_json_and_generation(method: qft_adder.Method, overflow: qft_adder.Overflow) -> None: + """Expose both operand representations with the same overflow contract.""" + benchmark = qft_adder.QFTAdder(qft_adder.Options(addend="110", accumulator="011", method=method, overflow=overflow)) + expected_sum = "1001" if overflow == qft_adder.Overflow.CARRY else "001" + expected = ("110" if method == qft_adder.Method.REGISTER else "") + expected_sum + assert benchmark.options.addend == "110" + assert benchmark.options.accumulator == "011" + assert benchmark.output.width == len(expected) + assert benchmark.expected_result == expected + assert benchmark.probability(expected) == 1 + assert benchmark.evaluate({expected: 8}).success_probability == 1 + parameters = json.loads(benchmark.instance_specification_json)["parameters"] + assert parameters["addend"] == "110" + assert parameters["accumulator"] == "011" + copy = qft_adder.QFTAdder.from_instance_specification_json(benchmark.instance_specification_json) + manifest_copy = qft_adder.QFTAdder.from_manifest_json(benchmark.manifest_json) + assert copy.case_id == manifest_copy.case_id == benchmark.case_id + assert_generates(benchmark.generate()) + + +def test_qft_adder_superposition_reference() -> None: + """Keep the observable correlation for a partly superposed addend.""" + benchmark = qft_adder.QFTAdder(qft_adder.Options(addend="1+0", accumulator="001")) + assert benchmark.expected_result is None + assert benchmark.probability("100101") == pytest.approx(0.5) + assert benchmark.probability("110111") == pytest.approx(0.5) + assert benchmark.probability("000001") == 0 + assert benchmark.probability("100100") == 0 + evaluation = benchmark.evaluate({"100101": 1, "110111": 1}) + assert evaluation.total_variation_distance == 0 + assert evaluation.success_probability is None diff --git a/test/python/test_cli.py b/test/python/test_cli.py index ad91946f6c..d375fcffd5 100644 --- a/test/python/test_cli.py +++ b/test/python/test_cli.py @@ -123,6 +123,7 @@ def test_benchmark_cli(script_runner: ScriptRunner) -> None: assert '"ghz"' in ret.stdout assert '"grover"' in ret.stdout assert '"multiplexer"' in ret.stdout + assert '"qft-adder"' in ret.stdout assert '"qpe"' in ret.stdout assert '"teleportation"' in ret.stdout