From ff0c962f489e52b7b3393c963b4e74f386ca9e91 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:21:30 +0200 Subject: [PATCH 01/24] Add quantum-input QFT adder benchmark Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/qft-adder-quantum.md | 50 +++ bindings/bench/CMakeLists.txt | 1 + bindings/bench/register_bench.cpp | 5 + bindings/bench/register_qft_adder_quantum.cpp | 85 ++++ docs/benchmarks.md | 14 + include/mqt-core/bench/BenchmarkFamilies.inc | 1 + include/mqt-core/bench/JSON.hpp | 1 + include/mqt-core/bench/QFTAdderQuantum.hpp | 46 +++ mlir/bench/programs/CMakeLists.txt | 2 + mlir/bench/programs/Programs.h | 5 + mlir/bench/programs/QFTAdderQuantum.cpp | 86 ++++ mlir/bench/programs/QFTAdderUtils.cpp | 90 +++++ mlir/bench/programs/QFTAdderUtils.h | 33 ++ mlir/include/mlir/bench/Generate.h | 5 + mlir/unittests/bench/CMakeLists.txt | 1 + mlir/unittests/bench/test_benchmark_cli.cmake | 4 +- .../bench/test_benchmark_generate.cpp | 2 + ...t_benchmark_generate_qft_adder_quantum.cpp | 370 ++++++++++++++++++ python/mqt/core/bench/__init__.pyi | 1 + python/mqt/core/bench/qft_adder_quantum.pyi | 63 +++ src/bench/JSON.cpp | 51 +++ src/bench/QFTAdderQuantum.cpp | 71 ++++ test/bench/test_json.cpp | 51 ++- test/bench/test_qft_adder_quantum.cpp | 78 ++++ test/python/test_bench.py | 241 ++++++++++++ test/python/test_cli.py | 1 + 26 files changed, 1355 insertions(+), 3 deletions(-) create mode 100644 .agent/plans/qft-adder-quantum.md create mode 100644 bindings/bench/register_qft_adder_quantum.cpp create mode 100644 include/mqt-core/bench/QFTAdderQuantum.hpp create mode 100644 mlir/bench/programs/QFTAdderQuantum.cpp create mode 100644 mlir/bench/programs/QFTAdderUtils.cpp create mode 100644 mlir/bench/programs/QFTAdderUtils.h create mode 100644 mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp create mode 100644 python/mqt/core/bench/qft_adder_quantum.pyi create mode 100644 src/bench/QFTAdderQuantum.cpp create mode 100644 test/bench/test_qft_adder_quantum.cpp create mode 100644 test/python/test_bench.py diff --git a/.agent/plans/qft-adder-quantum.md b/.agent/plans/qft-adder-quantum.md new file mode 100644 index 0000000000..e59ae2964c --- /dev/null +++ b/.agent/plans/qft-adder-quantum.md @@ -0,0 +1,50 @@ +# Add a quantum-input QFT adder benchmark + +Status: in progress. The implementation is validated. The draft pull request and +its changelog reference remain to be added. + +## Goal and scope + +Add the `qft-adder-quantum` structured benchmark from Draper's +[Addition on a Quantum Computer](https://arxiv.org/abs/quant-ph/0008033). The +benchmark must be available through the typed C++, JSON, command-line, Python, +and MLIR generation interfaces. It must generate the full no-swap QFT, Draper +addition, and inverse-QFT circuit rather than a circuit with the same output +distribution. + +The benchmark parameter is the width `n` of each quantum register. The source +register is prepared as |+>^n and the accumulator as |1>. The one logical +`result` output has width `2n` and is written as the big-endian concatenation +`addend || sum`. Its ideal distribution has probability `2^-n` exactly when +`sum = addend + 1 mod 2^n`. Measuring both registers keeps this correlation +observable; measuring the sum alone would produce an uninformative uniform +distribution. + +## Decisions + +Register index zero is the least-significant bit. The forward QFT uses no swaps +and visits targets from most to least significant. For target `t`, it applies H +and then `CP(pi / 2^(t-c))` from every lower control `c`. The addition block +applies the same controlled-phase gate from source control `c <= t` to +accumulator target `t`, including each `CP(pi)` gate. The inverse QFT reverses +the complete gate order and negates each phase. `CP` cannot be replaced with a +controlled RZ because their relative phases differ. + +The width is limited to 1024 qubits per register. This keeps the smallest +required binary phase and the ideal probability representable as `double`. The +implementation does not add swaps, carry qubits, approximate rotations, or an +alternative QFT convention. A private MLIR helper may own the shared forward and +inverse no-swap transforms; it must not change the existing QFT benchmark. + +## Work remaining + +- [ ] Create the draft stacked pull request and fold its number into the + existing unreleased structured-benchmark changelog entry. + +## Validation + +The release build, all 50 native benchmark tests, all 15 MLIR benchmark tests, +the benchmark CLI test, and 23 focused Python benchmark and CLI tests pass. The +Python test samples the width-three circuit and compares the result with the +analytic correlation. Stub generation, the general repository lint session, and +`git diff --check` pass. The separate C++ lint session was not run. diff --git a/bindings/bench/CMakeLists.txt b/bindings/bench/CMakeLists.txt index f3caed2f84..2027ed3d12 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_quantum.cpp register_qpe.cpp register_teleportation.cpp) diff --git a/bindings/bench/register_bench.cpp b/bindings/bench/register_bench.cpp index 03f5b51122..6e4ee3cd9c 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 registerQFTAdderQuantum(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_ qftAdderQuantum = m.def_submodule( + "qft_adder_quantum", "Quantum-input QFT adder instances and options."); + registerQFTAdderQuantum(qftAdderQuantum); + const nb::module_ qpe = m.def_submodule("qpe", "QPE benchmark instances and options."); registerQPE(qpe); diff --git a/bindings/bench/register_qft_adder_quantum.cpp b/bindings/bench/register_qft_adder_quantum.cpp new file mode 100644 index 0000000000..f8a86a0db4 --- /dev/null +++ b/bindings/bench/register_qft_adder_quantum.cpp @@ -0,0 +1,85 @@ +/* + * 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/QFTAdderQuantum.hpp" + +#include +#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 registerQFTAdderQuantum(const nb::module_& m) { + nb::class_( + m, "Options", "Parameters for a quantum-input QFT adder benchmark.") + .def(nb::init(), nb::kw_only(), "qubits"_a) + .def_ro("qubits", &bench::QFTAdderQuantumOptions::qubits, + "The number of qubits in each input register."); + + auto qftAdder = nb::class_( + m, "QFTAdderQuantum", "A validated quantum-input QFT adder benchmark."); + qftAdder.def(nb::init(), "options"_a) + .def_prop_ro("options", &bench::QFTAdderQuantum::options, + nb::rv_policy::reference_internal, + "The resolved benchmark parameters.") + .def_prop_ro( + "output", &bench::QFTAdderQuantum::output, + nb::rv_policy::reference_internal, + "The logical output register, with the addend followed by the sum.") + .def("probability", &bench::QFTAdderQuantum::probability, "outcome"_a, + "Return the ideal probability of an outcome.") + .def("evaluate", &bench::QFTAdderQuantum::evaluate, "counts"_a, + "Compare sampled counts with the ideal distribution.") + .def( + "generate", + [](const bench::QFTAdderQuantum& 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::QFTAdderQuantum& value) { + return bench::toInstanceSpecificationJSON(value); + }, + "The canonical instance specification JSON.") + .def_prop_ro( + "manifest_json", + [](const bench::QFTAdderQuantum& value) { + return bench::toManifestJSON(value); + }, + "The canonical manifest JSON.") + .def_prop_ro( + "case_id", + [](const bench::QFTAdderQuantum& value) { + return bench::caseId(value); + }, + "The stable semantic case ID.") + .def_static("from_instance_specification_json", + &bench::qftAdderQuantumFromInstanceSpecificationJSON, + "json"_a, nb::kw_only(), + "source"_a = "", + "Parse a strict benchmark instance specification.") + .def_static("from_manifest_json", &bench::qftAdderQuantumFromManifestJSON, + "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..61e6e2c79d 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -53,6 +53,20 @@ print("Width:", benchmark.output.width) Each family validates its instance when it creates one. Fixed families need no options. +## Quantum-input QFT adder + +The `qft-adder-quantum` family implements Draper's +[QFT adder](https://arxiv.org/abs/quant-ph/0008033). For a configured width `n`, +the benchmark prepares an `n`-qubit addend register in the uniform +superposition and an `n`-qubit accumulator in state |1>. It applies the exact +no-swap QFT to the accumulator, the complete controlled-phase addition, and the +inverse QFT. + +The `2n`-bit result is the big-endian concatenation `addend || sum`. An outcome +has probability `2^-n` when `sum = addend + 1 mod 2^n` and probability zero +otherwise. Keeping both registers in the result exposes the correlation that +defines the addition; the sum alone would be uniform. + ## Inspect the canonical instance specification and manifest A canonical instance specification records every resolved default. A manifest diff --git a/include/mqt-core/bench/BenchmarkFamilies.inc b/include/mqt-core/bench/BenchmarkFamilies.inc index 6657eaed60..c87149b77a 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(QFTAdderQuantum, qftAdderQuantum, "qft-adder-quantum", 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..7d6e13016c 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/QFTAdderQuantum.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" #include "bench/mqt_core_bench_export.h" diff --git a/include/mqt-core/bench/QFTAdderQuantum.hpp b/include/mqt-core/bench/QFTAdderQuantum.hpp new file mode 100644 index 0000000000..6f9b894dc0 --- /dev/null +++ b/include/mqt-core/bench/QFTAdderQuantum.hpp @@ -0,0 +1,46 @@ +/* + * 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 + +namespace mqt::bench { + +/// Parameters for one quantum-input QFT adder benchmark instance. +struct QFTAdderQuantumOptions { + static constexpr size_t MAX_QUBITS = 1'024; + + /// Number of qubits in each input register. + size_t qubits; +}; + +/// A validated quantum-input QFT adder and its analytic reference. +class MQT_CORE_BENCH_EXPORT QFTAdderQuantum final { +public: + explicit QFTAdderQuantum(QFTAdderQuantumOptions options); + + [[nodiscard]] const QFTAdderQuantumOptions& options() const noexcept; + [[nodiscard]] const Output& output() const noexcept; + /// Return the ideal probability of a big-endian 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: + QFTAdderQuantumOptions options_; + Output output_; +}; + +} // namespace mqt::bench diff --git a/mlir/bench/programs/CMakeLists.txt b/mlir/bench/programs/CMakeLists.txt index d16f06aa00..53d84f4147 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 + QFTAdderQuantum.cpp + QFTAdderUtils.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..c50d98b453 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 QFTAdderQuantum; 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 quantum-input QFT adder benchmark. +SmallVector qftAdderQuantum(qc::QCProgramBuilder& builder, + const QFTAdderQuantum& benchmark); + /// Emit one configured QPE benchmark. SmallVector qpe(qc::QCProgramBuilder& builder, const QPE& benchmark); diff --git a/mlir/bench/programs/QFTAdderQuantum.cpp b/mlir/bench/programs/QFTAdderQuantum.cpp new file mode 100644 index 0000000000..aeca0c6301 --- /dev/null +++ b/mlir/bench/programs/QFTAdderQuantum.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 "bench/QFTAdderQuantum.hpp" + +#include "Programs.h" +#include "QFTAdderUtils.h" +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace mqt::bench { + +using namespace mlir; + +static void addQuantumRegister(qc::QCProgramBuilder& builder, Value addend, + Value sum, int64_t qubits) { + auto zero = builder.indexConstant(0); + auto one = builder.indexConstant(1); + auto last = builder.indexConstant(qubits - 1); + 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(); + auto firstAngle = builder.floatConstant(std::numbers::pi); + auto half = builder.floatConstant(0.5); + auto loop = + scf::ForOp::create(builder, zero, upper, one, ValueRange{firstAngle}); + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(loop.getBody()); + auto angle = loop.getRegionIterArg(0); + auto control = + arith::SubIOp::create(builder, target, loop.getInductionVar()) + .getResult(); + builder.cp(angle, builder.loadQubit(addend, control), + builder.loadQubit(sum, target)); + auto next = arith::MulFOp::create(builder, angle, half).getResult(); + scf::YieldOp::create(builder, ValueRange{next}); + }); +} + +SmallVector qftAdderQuantum(qc::QCProgramBuilder& builder, + const QFTAdderQuantum& benchmark) { + const auto qubits = static_cast(benchmark.options().qubits); + auto addend = builder.allocQubitRegisterStorage(qubits, "addend"); + auto sum = builder.allocQubitRegisterStorage(qubits, "sum"); + auto result = builder.allocClassicalBitRegister( + static_cast(benchmark.output().width), benchmark.output().name); + + builder.scfFor(0, qubits, 1, [&](Value index) { + builder.h(builder.loadQubit(addend, index)); + }); + auto zero = builder.indexConstant(0); + builder.x(builder.loadQubit(sum, zero)); + + detail::forwardQFT(builder, sum, qubits); + addQuantumRegister(builder, addend, sum, qubits); + detail::inverseQFT(builder, sum, qubits); + + builder.scfFor(0, qubits, 1, [&](Value index) { + builder.measure(builder.loadQubit(sum, index), result, index); + }); + auto resultOffset = builder.indexConstant(qubits); + builder.scfFor(0, qubits, 1, [&](Value index) { + auto resultIndex = + arith::AddIOp::create(builder, resultOffset, index).getResult(); + builder.measure(builder.loadQubit(addend, index), result, resultIndex); + }); + return {result}; +} + +} // namespace mqt::bench diff --git a/mlir/bench/programs/QFTAdderUtils.cpp b/mlir/bench/programs/QFTAdderUtils.cpp new file mode 100644 index 0000000000..5e80c66fee --- /dev/null +++ b/mlir/bench/programs/QFTAdderUtils.cpp @@ -0,0 +1,90 @@ +/* + * 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 "QFTAdderUtils.h" + +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace mqt::bench::detail { + +using namespace mlir; + +static void +phaseRotationLoop(qc::QCProgramBuilder& builder, Value upper, + Value initialAngle, double factor, + const function_ref& body) { + auto zero = builder.indexConstant(0); + auto one = builder.indexConstant(1); + auto scale = builder.floatConstant(factor); + auto loop = + scf::ForOp::create(builder, zero, upper, one, 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 one = builder.indexConstant(1); + auto last = builder.indexConstant(qubits - 1); + 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(); + auto firstAngle = builder.floatConstant(std::numbers::pi / 2.); + phaseRotationLoop( + builder, target, firstAngle, 0.5, [&](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); + auto half = builder.floatConstant(0.5); + auto loop = + scf::ForOp::create(builder, zero, upper, one, ValueRange{firstAngle}); + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(loop.getBody()); + + auto target = loop.getInductionVar(); + auto initialAngle = loop.getRegionIterArg(0); + phaseRotationLoop( + builder, target, initialAngle, 2., [&](Value angle, Value control) { + builder.cp(angle, builder.loadQubit(qubitRegister, control), + builder.loadQubit(qubitRegister, target)); + }); + builder.h(builder.loadQubit(qubitRegister, target)); + + auto next = arith::MulFOp::create(builder, initialAngle, half).getResult(); + scf::YieldOp::create(builder, ValueRange{next}); +} + +} // namespace mqt::bench::detail diff --git a/mlir/bench/programs/QFTAdderUtils.h b/mlir/bench/programs/QFTAdderUtils.h new file mode 100644 index 0000000000..107eed1034 --- /dev/null +++ b/mlir/bench/programs/QFTAdderUtils.h @@ -0,0 +1,33 @@ +/* + * 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 + +namespace mlir { +class Value; + +namespace qc { +class QCProgramBuilder; +} // namespace qc +} // namespace mlir + +namespace mqt::bench::detail { + +/// Apply the exact no-swap QFT used by the QFT-adder families. +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/include/mlir/bench/Generate.h b/mlir/include/mlir/bench/Generate.h index a7ac604611..32bdd1b1e0 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 QFTAdderQuantum; 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 quantum-input QFT adder benchmark. +[[nodiscard]] std::optional +generate(const QFTAdderQuantum& 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..2fb3e60e83 100644 --- a/mlir/unittests/bench/CMakeLists.txt +++ b/mlir/unittests/bench/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable( test_benchmark_generate_grover.cpp test_benchmark_generate_multiplexer.cpp test_benchmark_generate_qft.cpp + test_benchmark_generate_qft_adder_quantum.cpp test_benchmark_generate_qpe.cpp test_benchmark_generate_teleportation.cpp) 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..ee26cde8aa 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/QFTAdderQuantum.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" #include "mlir/bench/Generate.h" @@ -44,6 +45,7 @@ TEST(GenerateProgramTest, GeneratesEveryBenchmarkMethodAsQCAndJeff) { expectValidQCAndJeff(QFT{{.qubits = 3, .periodExponent = 1}}); expectValidQCAndJeff(QFT{ {.qubits = 3, .periodExponent = 1, .method = QFTMethod::Semiclassical}}); + expectValidQCAndJeff(QFTAdderQuantum{{.qubits = 3}}); 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_quantum.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp new file mode 100644 index 0000000000..4fc92d0dbf --- /dev/null +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp @@ -0,0 +1,370 @@ +/* + * 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/QFTAdderQuantum.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 + +namespace mqt::bench { + +using namespace mlir; + +namespace { + +void expectConstantIndex(Value value, int64_t expected) { + auto constant = value.getDefiningOp(); + ASSERT_TRUE(constant); + EXPECT_EQ(constant.value(), expected); +} + +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); +} + +void expectStaticLoop(scf::ForOp loop, int64_t lower, int64_t upper) { + expectConstantIndex(loop.getLowerBound(), lower); + expectConstantIndex(loop.getUpperBound(), upper); + expectConstantIndex(loop.getStep(), 1); +} + +[[nodiscard]] SmallVector topLevelLoops(ModuleOp moduleOp) { + SmallVector loops; + moduleOp.walk([&](scf::ForOp loop) { + if (!loop->getParentOfType()) { + loops.push_back(loop); + } + }); + return loops; +} + +[[nodiscard]] SmallVector nestedLoops(scf::ForOp outer) { + SmallVector loops; + outer.walk([&](scf::ForOp loop) { + if (loop != outer) { + loops.push_back(loop); + } + }); + return loops; +} + +void expectAngleRecurrence(scf::ForOp loop, Value initialAngle, double factor) { + ASSERT_EQ(loop.getInitArgs().size(), 1U); + EXPECT_EQ(loop.getInitArgs().front(), initialAngle); + auto angle = loop.getRegionIterArg(0); + + auto yield = dyn_cast(loop.getBody()->getTerminator()); + ASSERT_TRUE(yield); + ASSERT_EQ(yield.getNumOperands(), 1U); + auto scale = yield.getOperand(0).getDefiningOp(); + ASSERT_TRUE(scale); + EXPECT_EQ(scale.getLhs(), angle); + expectConstantFloat(scale.getRhs(), factor); +} + +struct ControlledPhase { + qc::CtrlOp control; + qc::POp phase; +}; + +[[nodiscard]] ControlledPhase controlledPhase(scf::ForOp loop) { + ControlledPhase result; + size_t controls = 0; + size_t phases = 0; + loop.walk([&](qc::CtrlOp op) { + result.control = op; + ++controls; + }); + loop.walk([&](qc::POp op) { + result.phase = op; + ++phases; + }); + EXPECT_EQ(controls, 1U); + EXPECT_EQ(phases, 1U); + if (result.control) { + EXPECT_EQ(result.control.getNumControls(), 1U); + EXPECT_EQ(result.control.getNumTargets(), 1U); + } + return result; +} + +} // namespace + +TEST(GenerateProgramTest, EmitsExactQuantumQFTAdderSchedule) { + constexpr int64_t qubits = 3; + auto program = generate(QFTAdderQuantum{{.qubits = qubits}}); + 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); + EXPECT_EQ(test::countOps(moduleOp), 0U); + EXPECT_EQ(test::countOps(moduleOp), 9U); + size_t unitaries = 0; + moduleOp.walk([&](qc::UnitaryOpInterface /*unused*/) { ++unitaries; }); + EXPECT_EQ(unitaries, 10U); + + cbit::AllocOp resultAllocation; + moduleOp.walk([&](cbit::AllocOp op) { resultAllocation = op; }); + ASSERT_TRUE(resultAllocation); + EXPECT_EQ(resultAllocation.getResult().getType().getWidth(), 2 * qubits); + + auto loops = topLevelLoops(moduleOp); + ASSERT_EQ(loops.size(), 6U); + for (auto loop : loops) { + expectStaticLoop(loop, 0, qubits); + } + + qc::HOp prepareAddend; + loops[0].walk([&](qc::HOp op) { prepareAddend = op; }); + ASSERT_TRUE(prepareAddend); + auto addendLoad = prepareAddend.getQubit(0).getDefiningOp(); + ASSERT_TRUE(addendLoad); + EXPECT_EQ(addendLoad.getIndices().front(), loops[0].getInductionVar()); + auto addend = addendLoad.getMemref(); + + qc::XOp prepareOne; + moduleOp.walk([&](qc::XOp op) { prepareOne = op; }); + ASSERT_TRUE(prepareOne); + auto sumLoad = prepareOne.getQubit(0).getDefiningOp(); + ASSERT_TRUE(sumLoad); + expectConstantIndex(sumLoad.getIndices().front(), 0); + auto sum = sumLoad.getMemref(); + EXPECT_NE(addend, sum); + EXPECT_TRUE(loops[0]->isBeforeInBlock(prepareOne)); + EXPECT_TRUE(prepareOne->isBeforeInBlock(loops[1])); + + // The positive, no-swap QFT visits t = n - 1 ... 0. For each t, it + // applies H(y[t]) before CP(pi / 2^(t-c), y[c], y[t]) for c = t - 1 ... 0. + auto forward = loops[1]; + qc::HOp forwardH; + forward.walk([&](qc::HOp op) { forwardH = op; }); + ASSERT_TRUE(forwardH); + auto forwardTargetLoad = forwardH.getQubit(0).getDefiningOp(); + ASSERT_TRUE(forwardTargetLoad); + EXPECT_EQ(forwardTargetLoad.getMemref(), sum); + auto forwardTarget = forwardTargetLoad.getIndices().front(); + auto forwardTargetExpression = forwardTarget.getDefiningOp(); + ASSERT_TRUE(forwardTargetExpression); + expectConstantIndex(forwardTargetExpression.getLhs(), qubits - 1); + EXPECT_EQ(forwardTargetExpression.getRhs(), forward.getInductionVar()); + + auto forwardInnerLoops = nestedLoops(forward); + ASSERT_EQ(forwardInnerLoops.size(), 1U); + auto forwardInner = forwardInnerLoops.front(); + EXPECT_TRUE(forwardH->isBeforeInBlock(forwardInner)); + expectConstantIndex(forwardInner.getLowerBound(), 0); + EXPECT_EQ(forwardInner.getUpperBound(), forwardTarget); + expectConstantIndex(forwardInner.getStep(), 1); + ASSERT_EQ(forwardInner.getInitArgs().size(), 1U); + expectConstantFloat(forwardInner.getInitArgs().front(), + std::numbers::pi / 2.); + expectAngleRecurrence(forwardInner, forwardInner.getInitArgs().front(), 0.5); + auto forwardPhase = controlledPhase(forwardInner); + ASSERT_TRUE(forwardPhase.control); + ASSERT_TRUE(forwardPhase.phase); + EXPECT_EQ(forwardPhase.phase.getTheta(), forwardInner.getRegionIterArg(0)); + auto forwardControl = + forwardPhase.control.getControl(0).getDefiningOp(); + auto forwardPhaseTarget = + forwardPhase.control.getTarget(0).getDefiningOp(); + ASSERT_TRUE(forwardControl); + ASSERT_TRUE(forwardPhaseTarget); + EXPECT_EQ(forwardControl.getMemref(), sum); + EXPECT_EQ(forwardPhaseTarget.getMemref(), sum); + EXPECT_EQ(forwardPhaseTarget.getIndices().front(), forwardTarget); + auto forwardControlExpression = + forwardControl.getIndices().front().getDefiningOp(); + ASSERT_TRUE(forwardControlExpression); + EXPECT_EQ(forwardControlExpression.getRhs(), forwardInner.getInductionVar()); + auto previous = + forwardControlExpression.getLhs().getDefiningOp(); + ASSERT_TRUE(previous); + expectConstantIndex(previous.getLhs(), qubits - 2); + EXPECT_EQ(previous.getRhs(), forward.getInductionVar()); + + // Draper's addition visits t = n - 1 ... 0 and c = t ... 0. The first + // gate at each target is CP(pi), including the matching source bit. + auto addition = loops[2]; + EXPECT_TRUE(forward->isBeforeInBlock(addition)); + auto additionInnerLoops = nestedLoops(addition); + ASSERT_EQ(additionInnerLoops.size(), 1U); + auto additionInner = additionInnerLoops.front(); + expectConstantIndex(additionInner.getLowerBound(), 0); + auto additionUpper = + additionInner.getUpperBound().getDefiningOp(); + ASSERT_TRUE(additionUpper); + expectConstantIndex(additionUpper.getLhs(), qubits); + EXPECT_EQ(additionUpper.getRhs(), addition.getInductionVar()); + expectConstantIndex(additionInner.getStep(), 1); + ASSERT_EQ(additionInner.getInitArgs().size(), 1U); + expectConstantFloat(additionInner.getInitArgs().front(), std::numbers::pi); + expectAngleRecurrence(additionInner, additionInner.getInitArgs().front(), + 0.5); + auto additionPhase = controlledPhase(additionInner); + ASSERT_TRUE(additionPhase.control); + ASSERT_TRUE(additionPhase.phase); + EXPECT_EQ(additionPhase.phase.getTheta(), additionInner.getRegionIterArg(0)); + auto sourceControl = + additionPhase.control.getControl(0).getDefiningOp(); + auto sumTarget = + additionPhase.control.getTarget(0).getDefiningOp(); + ASSERT_TRUE(sourceControl); + ASSERT_TRUE(sumTarget); + auto additionTarget = sumTarget.getIndices().front(); + auto descendingAdditionTarget = additionTarget.getDefiningOp(); + ASSERT_TRUE(descendingAdditionTarget); + expectConstantIndex(descendingAdditionTarget.getLhs(), qubits - 1); + EXPECT_EQ(descendingAdditionTarget.getRhs(), addition.getInductionVar()); + EXPECT_EQ(sourceControl.getMemref(), addend); + EXPECT_EQ(sumTarget.getMemref(), sum); + EXPECT_EQ(sumTarget.getIndices().front(), additionTarget); + auto sourceIndex = + sourceControl.getIndices().front().getDefiningOp(); + ASSERT_TRUE(sourceIndex); + EXPECT_EQ(sourceIndex.getLhs(), additionTarget); + EXPECT_EQ(sourceIndex.getRhs(), additionInner.getInductionVar()); + + // The inverse reverses every QFT gate: c = 0 ... t - 1, then H(y[t]), + // while its first inner-loop angle progresses as -pi / 2^t. + auto inverse = loops[3]; + EXPECT_TRUE(addition->isBeforeInBlock(inverse)); + ASSERT_EQ(inverse.getInitArgs().size(), 1U); + expectConstantFloat(inverse.getInitArgs().front(), -std::numbers::pi); + auto inverseInnerLoops = nestedLoops(inverse); + ASSERT_EQ(inverseInnerLoops.size(), 1U); + auto inverseInner = inverseInnerLoops.front(); + expectConstantIndex(inverseInner.getLowerBound(), 0); + EXPECT_EQ(inverseInner.getUpperBound(), inverse.getInductionVar()); + expectConstantIndex(inverseInner.getStep(), 1); + expectAngleRecurrence(inverseInner, inverse.getRegionIterArg(0), 2.); + auto inversePhase = controlledPhase(inverseInner); + ASSERT_TRUE(inversePhase.control); + ASSERT_TRUE(inversePhase.phase); + EXPECT_EQ(inversePhase.phase.getTheta(), inverseInner.getRegionIterArg(0)); + auto inverseControl = + inversePhase.control.getControl(0).getDefiningOp(); + auto inverseTarget = + inversePhase.control.getTarget(0).getDefiningOp(); + ASSERT_TRUE(inverseControl); + ASSERT_TRUE(inverseTarget); + EXPECT_EQ(inverseControl.getMemref(), sum); + EXPECT_EQ(inverseControl.getIndices().front(), + inverseInner.getInductionVar()); + EXPECT_EQ(inverseTarget.getMemref(), sum); + EXPECT_EQ(inverseTarget.getIndices().front(), inverse.getInductionVar()); + qc::HOp inverseH; + inverse.walk([&](qc::HOp op) { inverseH = op; }); + ASSERT_TRUE(inverseH); + EXPECT_TRUE(inverseInner->isBeforeInBlock(inverseH)); + auto inverseHLoad = inverseH.getQubit(0).getDefiningOp(); + ASSERT_TRUE(inverseHLoad); + EXPECT_EQ(inverseHLoad.getMemref(), sum); + EXPECT_EQ(inverseHLoad.getIndices().front(), inverse.getInductionVar()); + auto inverseYield = + dyn_cast(inverse.getBody()->getTerminator()); + ASSERT_TRUE(inverseYield); + ASSERT_EQ(inverseYield.getNumOperands(), 1U); + auto nextInverseAngle = + inverseYield.getOperand(0).getDefiningOp(); + ASSERT_TRUE(nextInverseAngle); + EXPECT_EQ(nextInverseAngle.getLhs(), inverse.getRegionIterArg(0)); + expectConstantFloat(nextInverseAngle.getRhs(), 0.5); + + // Register bit zero is least significant. Measuring sum at i and addend at + // n + i makes the displayed big-endian result `addend || sum`. + auto sumMeasurementLoop = loops[4]; + auto addendMeasurementLoop = loops[5]; + EXPECT_TRUE(inverse->isBeforeInBlock(sumMeasurementLoop)); + EXPECT_TRUE(sumMeasurementLoop->isBeforeInBlock(addendMeasurementLoop)); + qc::MeasureOp sumMeasurement; + sumMeasurementLoop.walk([&](qc::MeasureOp op) { sumMeasurement = op; }); + qc::MeasureOp addendMeasurement; + addendMeasurementLoop.walk([&](qc::MeasureOp op) { addendMeasurement = op; }); + ASSERT_TRUE(sumMeasurement); + ASSERT_TRUE(addendMeasurement); + auto measuredSum = sumMeasurement.getQubit().getDefiningOp(); + auto measuredAddend = + addendMeasurement.getQubit().getDefiningOp(); + ASSERT_TRUE(measuredSum); + ASSERT_TRUE(measuredAddend); + EXPECT_EQ(measuredSum.getMemref(), sum); + EXPECT_EQ(measuredSum.getIndices().front(), + sumMeasurementLoop.getInductionVar()); + EXPECT_EQ(measuredAddend.getMemref(), addend); + EXPECT_EQ(measuredAddend.getIndices().front(), + addendMeasurementLoop.getInductionVar()); + + auto sumStore = + dyn_cast(*sumMeasurement.getResult().user_begin()); + auto addendStore = + dyn_cast(*addendMeasurement.getResult().user_begin()); + ASSERT_TRUE(sumStore); + ASSERT_TRUE(addendStore); + EXPECT_EQ(sumStore.getReg(), resultAllocation.getResult()); + EXPECT_EQ(sumStore.getIndex(), sumMeasurementLoop.getInductionVar()); + EXPECT_EQ(addendStore.getReg(), resultAllocation.getResult()); + auto displayedAddendIndex = + addendStore.getIndex().getDefiningOp(); + ASSERT_TRUE(displayedAddendIndex); + if (displayedAddendIndex.getLhs() == + addendMeasurementLoop.getInductionVar()) { + expectConstantIndex(displayedAddendIndex.getRhs(), qubits); + } else { + expectConstantIndex(displayedAddendIndex.getLhs(), qubits); + EXPECT_EQ(displayedAddendIndex.getRhs(), + addendMeasurementLoop.getInductionVar()); + } +} + +TEST(GenerateProgramTest, KeepsLargestQuantumQFTAdderFiniteAndStructured) { + auto program = + generate(QFTAdderQuantum{{.qubits = QFTAdderQuantumOptions::MAX_QUBITS}}); + 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())); + } + }); + test::expectJeffRoundTrip(std::move(*program)); +} + +} // namespace mqt::bench diff --git a/python/mqt/core/bench/__init__.pyi b/python/mqt/core/bench/__init__.pyi index 80101754bd..a687ed8291 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_quantum as qft_adder_quantum 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_quantum.pyi b/python/mqt/core/bench/qft_adder_quantum.pyi new file mode 100644 index 0000000000..fcc0c01dd1 --- /dev/null +++ b/python/mqt/core/bench/qft_adder_quantum.pyi @@ -0,0 +1,63 @@ +# 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 + +"""Quantum-input QFT adder instances and options.""" + +from collections.abc import Mapping + +import mqt.core.bench +import mqt.core.mlir + +class Options: + """Parameters for a quantum-input QFT adder benchmark.""" + + def __init__(self, *, qubits: int) -> None: ... + @property + def qubits(self) -> int: + """The number of qubits in each input register.""" + +class QFTAdderQuantum: + """A validated quantum-input QFT adder benchmark.""" + + 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, with the addend followed by the sum.""" + + 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 = "") -> QFTAdderQuantum: + """Parse a strict benchmark instance specification.""" + + @staticmethod + def from_manifest_json(json: str, *, source: str = "") -> QFTAdderQuantum: + """Parse a strict benchmark manifest.""" diff --git a/src/bench/JSON.cpp b/src/bench/JSON.cpp index c06fe1b495..3c03659cfd 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/QFTAdderQuantum.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -423,6 +424,21 @@ parseMultiplexerParameters(const Json& parameters, } } +[[nodiscard]] QFTAdderQuantum +parseQFTAdderQuantumParameters(const Json& parameters, + const std::string_view source) { + rejectUnknownKeys(parameters, {"qubits"}, source, "$/parameters"); + try { + return QFTAdderQuantum({ + .qubits = + sizeValue(required(parameters, "qubits", source, "$/parameters"), + source, "$/parameters/qubits"), + }); + } 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 +543,10 @@ parseTeleportationParameters(const Json& parameters, }; } +[[nodiscard]] Json parametersJSON(const QFTAdderQuantum& benchmark) { + return {{"qubits", benchmark.options().qubits}}; +} + [[nodiscard]] Json parametersJSON(const QPE& benchmark) { const auto& options = benchmark.options(); return { @@ -598,6 +618,16 @@ parseTeleportationParameters(const Json& parameters, }; } +[[nodiscard]] Json referenceJSON(const QFTAdderQuantum& benchmark) { + return { + {"kind", "analytic"}, + {"model", "qft_adder_quantum"}, + {"outcome_order", "big_endian"}, + {"output", benchmark.output().name}, + {"version", 1}, + }; +} + [[nodiscard]] Json referenceJSON(const QPE& benchmark) { return { {"kind", "analytic"}, @@ -876,6 +906,27 @@ template }); } +[[nodiscard]] Json qftAdderQuantumInstanceSpecificationSchema() { + return baseInstanceSpecificationSchema({ + {"additionalProperties", false}, + { + "properties", + { + { + "qubits", + { + {"maximum", QFTAdderQuantumOptions::MAX_QUBITS}, + {"minimum", 1}, + {"type", "integer"}, + }, + }, + }, + }, + {"required", {"qubits"}}, + {"type", "object"}, + }); +} + [[nodiscard]] Json qpeInstanceSpecificationSchema() { return baseInstanceSpecificationSchema({ {"additionalProperties", false}, diff --git a/src/bench/QFTAdderQuantum.cpp b/src/bench/QFTAdderQuantum.cpp new file mode 100644 index 0000000000..e3c3f76554 --- /dev/null +++ b/src/bench/QFTAdderQuantum.cpp @@ -0,0 +1,71 @@ +/* + * 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/QFTAdderQuantum.hpp" + +#include "EvaluationUtils.hpp" +#include "bench/Evaluation.hpp" + +#include +#include +#include + +namespace mqt::bench { +namespace { + +[[nodiscard]] bool isIncrement(const std::string_view addend, + const std::string_view sum) { + auto carry = true; + for (size_t index = addend.size(); index > 0; --index) { + const auto addendBit = addend[index - 1] == '1'; + const auto expectedSumBit = addendBit != carry; + if ((sum[index - 1] == '1') != expectedSumBit) { + return false; + } + carry = addendBit && carry; + } + return true; +} + +} // namespace + +QFTAdderQuantum::QFTAdderQuantum(QFTAdderQuantumOptions options) + : options_(options), + output_{.name = "result", .width = 2 * options_.qubits} { + if (options_.qubits == 0 || + options_.qubits > QFTAdderQuantumOptions::MAX_QUBITS) { + throw std::invalid_argument( + "quantum QFT adder qubits must be between 1 and 1024"); + } +} + +const QFTAdderQuantumOptions& QFTAdderQuantum::options() const noexcept { + return options_; +} + +const Output& QFTAdderQuantum::output() const noexcept { return output_; } + +double QFTAdderQuantum::probability(const std::string_view outcome) const { + detail::validateOutcome(outcome, output_.width); + const auto addend = outcome.substr(0, options_.qubits); + const auto sum = outcome.substr(options_.qubits); + if (!isIncrement(addend, sum)) { + return 0.; + } + return std::ldexp(1., -static_cast(options_.qubits)); +} + +Evaluation QFTAdderQuantum::evaluate(const Counts& counts) const { + return detail::evaluate( + output_, counts, + [this](const std::string_view outcome) { return probability(outcome); }); +} + +} // namespace mqt::bench diff --git a/test/bench/test_json.cpp b/test/bench/test_json.cpp index ca3a4d6f9a..f36de4160b 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/QFTAdderQuantum.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -56,6 +57,9 @@ using mqt::bench::multiplexerFromInstanceSpecificationJSON; using mqt::bench::multiplexerFromManifestJSON; using mqt::bench::Phase; using mqt::bench::QFT; +using mqt::bench::QFTAdderQuantum; +using mqt::bench::qftAdderQuantumFromInstanceSpecificationJSON; +using mqt::bench::qftAdderQuantumFromManifestJSON; using mqt::bench::qftFromInstanceSpecificationJSON; using mqt::bench::qftFromManifestJSON; using mqt::bench::QFTMethod; @@ -119,6 +123,13 @@ TEST(BenchmarkJSON, toInstanceSpecificationJSON(qft), R"({"benchmark":"qft","parameters":{"method":"standard","period_exponent":2,"qubits":4},"schema_version":1})"); + const auto qftAdderQuantum = qftAdderQuantumFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"qft-adder-quantum","parameters":{"qubits":3}})"); + EXPECT_EQ(qftAdderQuantum.options().qubits, 3); + EXPECT_EQ( + toInstanceSpecificationJSON(qftAdderQuantum), + R"({"benchmark":"qft-adder-quantum","parameters":{"qubits":3},"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 +153,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { const Multiplexer multiplexer{{.qubits = 7}}; const QFT qft{ {.qubits = 4, .periodExponent = 2, .method = QFTMethod::Semiclassical}}; + const QFTAdderQuantum qftAdderQuantum{{.qubits = 3}}; const QPE qpe{ {.precision = 5, .phase = Phase(1, 3), .method = QPEMethod::Iterative}}; const Teleportation teleportation; @@ -151,6 +163,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { const auto groverManifest = toManifestJSON(grover); const auto multiplexerManifest = toManifestJSON(multiplexer); const auto qftManifest = toManifestJSON(qft); + const auto qftAdderQuantumManifest = toManifestJSON(qftAdderQuantum); const auto qpeManifest = toManifestJSON(qpe); const auto teleportationManifest = toManifestJSON(teleportation); EXPECT_EQ(toManifestJSON(bvFromManifestJSON(bvManifest)), bvManifest); @@ -160,6 +173,9 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { EXPECT_EQ(toManifestJSON(multiplexerFromManifestJSON(multiplexerManifest)), multiplexerManifest); EXPECT_EQ(toManifestJSON(qftFromManifestJSON(qftManifest)), qftManifest); + EXPECT_EQ( + toManifestJSON(qftAdderQuantumFromManifestJSON(qftAdderQuantumManifest)), + qftAdderQuantumManifest); EXPECT_EQ(toManifestJSON(qpeFromManifestJSON(qpeManifest)), qpeManifest); EXPECT_EQ( toManifestJSON(teleportationFromManifestJSON(teleportationManifest)), @@ -169,6 +185,8 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { EXPECT_EQ(benchmarkIdFromManifestJSON(groverManifest), "grover"); EXPECT_EQ(benchmarkIdFromManifestJSON(multiplexerManifest), "multiplexer"); EXPECT_EQ(benchmarkIdFromManifestJSON(qftManifest), "qft"); + EXPECT_EQ(benchmarkIdFromManifestJSON(qftAdderQuantumManifest), + "qft-adder-quantum"); EXPECT_EQ(benchmarkIdFromManifestJSON(qpeManifest), "qpe"); EXPECT_EQ(benchmarkIdFromManifestJSON(teleportationManifest), "teleportation"); @@ -178,6 +196,9 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { std::string::npos); EXPECT_NE(multiplexerManifest.find("\"model\":\"multiplexer\""), std::string::npos); + EXPECT_NE(qftAdderQuantumManifest.find("\"model\":\"qft_adder_quantum\""), + std::string::npos); + EXPECT_NE(qftAdderQuantumManifest.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 +217,10 @@ TEST(BenchmarkJSON, UsesStableSemanticCaseIds) { caseId(QFT{{.qubits = 3, .periodExponent = 1, .method = QFTMethod::Semiclassical}})); + EXPECT_EQ(caseId(QFTAdderQuantum{{.qubits = 3}}), + caseId(QFTAdderQuantum{{.qubits = 3}})); + EXPECT_NE(caseId(QFTAdderQuantum{{.qubits = 3}}), + caseId(QFTAdderQuantum{{.qubits = 4}})); EXPECT_EQ(caseId(Multiplexer{{.qubits = 7}}), caseId(Multiplexer{{.qubits = 7}})); EXPECT_NE(caseId(Multiplexer{{.qubits = 7}}), @@ -275,6 +300,18 @@ TEST(BenchmarkJSON, R"({"schema_version":1,"benchmark":"multiplexer","parameters":{"qubits":7,"angles":[]}})")); }, "unknown key 'angles'"); + expectInvalid( + [] { + static_cast(qftAdderQuantumFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"qft-adder-quantum","parameters":{"qubits":0}})")); + }, + "between 1 and 1024"); + expectInvalid( + [] { + static_cast(qftAdderQuantumFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"qft-adder-quantum","parameters":{"qubits":3,"addend":"1"}})")); + }, + "unknown key 'addend'"); expectInvalid( [] { static_cast(teleportationFromInstanceSpecificationJSON( @@ -341,12 +378,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-quantum"},{"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 qftAdderQuantum = describeBenchmarkJSON("qft-adder-quantum"); const auto qpe = describeBenchmarkJSON("qpe"); const auto teleportation = describeBenchmarkJSON("teleportation"); EXPECT_NE(ghz.find("https://json-schema.org/draft/2020-12/schema"), @@ -359,6 +397,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(qftAdderQuantum.find("\"maximum\":1024"), std::string::npos); + EXPECT_NE(qftAdderQuantum.find("\"minimum\":1"), std::string::npos); EXPECT_NE(qpe.find("\"iterative\""), std::string::npos); EXPECT_NE( teleportation.find( @@ -397,6 +437,15 @@ TEST(BenchmarkJSON, ParsesCountsAndSerializesEvaluations) { EXPECT_NE(multiplexerEvaluation.find("\"total_variation_distance\":"), std::string::npos); + const QFTAdderQuantum qftAdderQuantum{{.qubits = 2}}; + const auto qftAdderQuantumEvaluation = evaluateJSON( + toManifestJSON(qftAdderQuantum), + R"({"schema_version":1,"counts":{"0001":1,"0110":1,"1011":1,"1100":1}})"); + EXPECT_NE(qftAdderQuantumEvaluation.find("\"success_probability\":null"), + std::string::npos); + EXPECT_NE(qftAdderQuantumEvaluation.find("\"total_variation_distance\":0.0"), + std::string::npos); + const Teleportation teleportation; const auto teleportationEvaluation = evaluateJSON(toManifestJSON(teleportation), diff --git a/test/bench/test_qft_adder_quantum.cpp b/test/bench/test_qft_adder_quantum.cpp new file mode 100644 index 0000000000..7bd57737e6 --- /dev/null +++ b/test/bench/test_qft_adder_quantum.cpp @@ -0,0 +1,78 @@ +/* + * 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/Evaluation.hpp" +#include "bench/QFTAdderQuantum.hpp" + +#include + +#include +#include + +namespace { + +using mqt::bench::Output; +using mqt::bench::QFTAdderQuantum; +using mqt::bench::QFTAdderQuantumOptions; + +TEST(QFTAdderQuantum, StoresTheRegisterWidthAndOutput) { + const QFTAdderQuantum benchmark{{.qubits = 7}}; + EXPECT_EQ(benchmark.options().qubits, 7); + EXPECT_EQ(benchmark.output(), (Output{"result", 14})); +} + +TEST(QFTAdderQuantum, ValidatesTheConfiguredInstance) { + EXPECT_THROW(static_cast(QFTAdderQuantum{{.qubits = 0}}), + std::invalid_argument); + EXPECT_NO_THROW(static_cast( + QFTAdderQuantum{{.qubits = QFTAdderQuantumOptions::MAX_QUBITS}})); + EXPECT_THROW(static_cast(QFTAdderQuantum{ + {.qubits = QFTAdderQuantumOptions::MAX_QUBITS + 1}}), + std::invalid_argument); +} + +TEST(QFTAdderQuantum, GivesUniformWeightToCorrelatedSums) { + const QFTAdderQuantum benchmark{{.qubits = 3}}; + for (const auto* outcome : {"000001", "001010", "010011", "011100", "100101", + "101110", "110111", "111000"}) { + EXPECT_DOUBLE_EQ(benchmark.probability(outcome), 1. / 8.); + } + + EXPECT_DOUBLE_EQ(benchmark.probability("000000"), 0.); + EXPECT_DOUBLE_EQ(benchmark.probability("111111"), 0.); + EXPECT_THROW(static_cast(benchmark.probability("00001")), + std::invalid_argument); + EXPECT_THROW(static_cast(benchmark.probability("00000x")), + std::invalid_argument); +} + +TEST(QFTAdderQuantum, EvaluatesTheReferenceWithoutASuccessOutcome) { + const QFTAdderQuantum benchmark{{.qubits = 2}}; + const auto exact = + benchmark.evaluate({{"0001", 1}, {"0110", 1}, {"1011", 1}, {"1100", 1}}); + EXPECT_DOUBLE_EQ(exact.totalVariationDistance, 0.); + EXPECT_DOUBLE_EQ(exact.squaredHellingerFidelity, 1.); + EXPECT_FALSE(exact.successProbability); + + const auto biased = benchmark.evaluate({{"0001", 4}}); + EXPECT_DOUBLE_EQ(biased.totalVariationDistance, 0.75); + EXPECT_DOUBLE_EQ(biased.squaredHellingerFidelity, 0.25); + EXPECT_FALSE(biased.successProbability); +} + +TEST(QFTAdderQuantum, KeepsTheLargestReferenceWeightRepresentable) { + const QFTAdderQuantum benchmark{ + {.qubits = QFTAdderQuantumOptions::MAX_QUBITS}}; + const auto outcome = std::string(QFTAdderQuantumOptions::MAX_QUBITS, '1') + + std::string(QFTAdderQuantumOptions::MAX_QUBITS, '0'); + EXPECT_GT(benchmark.probability(outcome), 0.); +} + +} // namespace diff --git a/test/python/test_bench.py b/test/python/test_bench.py new file mode 100644 index 0000000000..d8f0247958 --- /dev/null +++ b/test/python/test_bench.py @@ -0,0 +1,241 @@ +# 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 + +"""Tests for typed benchmark instances and analytic references.""" + +from __future__ import annotations + +import json +from fractions import Fraction + +import pytest + +from mqt.core import bench, mlir +from mqt.core.bench import bv, ghz, grover, multiplexer, qft, qft_adder_quantum, qpe, teleportation + + +def assert_generates( + benchmark: ( + bv.BV + | ghz.GHZ + | grover.Grover + | multiplexer.Multiplexer + | qft.QFT + | qft_adder_quantum.QFTAdderQuantum + | qpe.QPE + | teleportation.Teleportation + ), +) -> None: + """Exercise the shared Python-to-MLIR generation boundary.""" + program = benchmark.generate() + assert isinstance(program, mlir.QCProgram) + assert "qc." in program.ir + assert isinstance(program.to_qco(), mlir.QCOProgram) + + +def test_bv_methods_share_the_hidden_string_reference() -> None: + """Expose static and dynamic Bernstein--Vazirani as one family.""" + for method in (bv.Method.STATIC, bv.Method.DYNAMIC): + benchmark = bv.BV(bv.Options(hidden_bitstring="101", method=method)) + assert benchmark.probability("101") == 1 + assert benchmark.evaluate({"101": 10}).success_probability == 1 + assert bv.BV.from_manifest_json(benchmark.manifest_json).case_id == benchmark.case_id + assert_generates(benchmark) + + +def test_ghz_options_reference_and_json_roundtrip() -> None: + """Keep GHZ parameters typed and preserve one semantic case through JSON.""" + with pytest.raises(TypeError): + ghz.Options(3) # ty: ignore[missing-argument, too-many-positional-arguments] + + options = ghz.Options( + qubits=3, + topology=ghz.Topology.STAR, + basis=ghz.Basis.X, + ) + with pytest.raises(AttributeError): + options.qubits = 4 # ty: ignore[invalid-assignment] + + benchmark = ghz.GHZ(options) + assert isinstance(benchmark.output, bench.Output) + assert benchmark.output.name == "result" + assert benchmark.output.width == 3 + assert benchmark.probability("011") == pytest.approx(0.25) + assert benchmark.probability("111") == 0 + + evaluation = benchmark.evaluate({"000": 50, "011": 50}) + assert isinstance(evaluation, bench.Evaluation) + assert evaluation.total_variation_distance == pytest.approx(0.5) + assert evaluation.squared_hellinger_fidelity == pytest.approx(0.5) + assert evaluation.success_probability is None + + instance_copy = ghz.GHZ.from_instance_specification_json(benchmark.instance_specification_json) + manifest_copy = ghz.GHZ.from_manifest_json(benchmark.manifest_json) + assert instance_copy.instance_specification_json == benchmark.instance_specification_json + assert manifest_copy.manifest_json == benchmark.manifest_json + assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id + assert_generates(benchmark) + + +def test_grover_resolves_iterations_and_reports_success() -> None: + """Expose Grover's resolved default and marked-outcome score.""" + options = grover.Options(marked_bitstring="10") + benchmark = grover.Grover(options) + + assert options.iterations is None + assert benchmark.options.iterations == 1 + assert benchmark.qubits == 2 + assert benchmark.probability("10") == pytest.approx(1) + assert benchmark.evaluate({"10": 20}).success_probability == pytest.approx(1) + + copy = grover.Grover.from_manifest_json(benchmark.manifest_json) + assert copy.instance_specification_json == benchmark.instance_specification_json + assert copy.case_id == benchmark.case_id + assert_generates(benchmark) + + +def test_multiplexer_reference_json_and_generation() -> None: + """Expose the fixed-angle quantum multiplexer as one typed family.""" + benchmark = multiplexer.Multiplexer(multiplexer.Options(qubits=3)) + assert benchmark.output.name == "result" + assert benchmark.output.width == 3 + assert benchmark.probability("000") == pytest.approx(0.25) + assert benchmark.probability("001") == 0 + + evaluation = benchmark.evaluate({"000": 10}) + assert evaluation.total_variation_distance == pytest.approx(0.75) + assert evaluation.squared_hellinger_fidelity == pytest.approx(0.25) + assert evaluation.success_probability is None + assert json.loads(benchmark.instance_specification_json)["parameters"] == {"qubits": 3} + + instance_copy = multiplexer.Multiplexer.from_instance_specification_json(benchmark.instance_specification_json) + manifest_copy = multiplexer.Multiplexer.from_manifest_json(benchmark.manifest_json) + assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id + + shots = 16_384 + counts = mlir.sample(benchmark.generate(), shots=shots, seed=17) + assert sum(counts.values()) == shots + assert benchmark.evaluate(counts).total_variation_distance < 0.03 + assert_generates(benchmark) + + +def test_qft_methods_share_the_periodic_reference() -> None: + """Expose standard and semiclassical QFT as one family.""" + for method in (qft.Method.STANDARD, qft.Method.SEMICLASSICAL): + benchmark = qft.QFT(qft.Options(qubits=3, period_exponent=1, method=method)) + assert benchmark.probability("000") == pytest.approx(0.5) + assert benchmark.probability("100") == pytest.approx(0.5) + assert ( + qft.QFT.from_instance_specification_json(benchmark.instance_specification_json).case_id == benchmark.case_id + ) + assert_generates(benchmark) + + +def test_quantum_qft_adder_reference_json_and_generation() -> None: + """Expose the correlated addend and sum distribution.""" + benchmark = qft_adder_quantum.QFTAdderQuantum(qft_adder_quantum.Options(qubits=2)) + assert benchmark.output.name == "result" + assert benchmark.output.width == 4 + assert benchmark.probability("0001") == pytest.approx(0.25) + assert benchmark.probability("0110") == pytest.approx(0.25) + assert benchmark.probability("1011") == pytest.approx(0.25) + assert benchmark.probability("1100") == pytest.approx(0.25) + assert benchmark.probability("0000") == 0 + + evaluation = benchmark.evaluate({"0001": 1, "0110": 1, "1011": 1, "1100": 1}) + assert evaluation.total_variation_distance == pytest.approx(0) + assert evaluation.squared_hellinger_fidelity == pytest.approx(1) + assert evaluation.success_probability is None + assert json.loads(benchmark.instance_specification_json)["parameters"] == {"qubits": 2} + + instance_copy = qft_adder_quantum.QFTAdderQuantum.from_instance_specification_json( + benchmark.instance_specification_json + ) + manifest_copy = qft_adder_quantum.QFTAdderQuantum.from_manifest_json(benchmark.manifest_json) + assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id + + sampled = qft_adder_quantum.QFTAdderQuantum(qft_adder_quantum.Options(qubits=3)) + shots = 16_384 + counts = sampled.generate().to_qco().sample(shots=shots, seed=17) + assert sum(counts.values()) == shots + assert sampled.evaluate(counts).total_variation_distance < 0.03 + assert_generates(benchmark) + + +def test_qpe_accepts_fraction_and_native_phase() -> None: + """Use exact rational input without a free-form parameter dictionary.""" + options = qpe.Options( + precision=2, + phase=Fraction(3, 24), + method=qpe.Method.ITERATIVE, + ) + assert options.phase == Fraction(1, 8) + + benchmark = qpe.QPE(options) + assert benchmark.probability("00") == pytest.approx((2 + 2**0.5) / 8) + assert benchmark.probability("01") == pytest.approx((2 + 2**0.5) / 8) + assert json.loads(benchmark.instance_specification_json)["parameters"]["phase"] == { + "denominator": 8, + "numerator": 1, + } + + instance_copy = qpe.QPE.from_instance_specification_json(benchmark.instance_specification_json) + assert instance_copy.options.phase == Fraction(1, 8) + assert instance_copy.options.method is qpe.Method.ITERATIVE + assert instance_copy.case_id == benchmark.case_id + + phase = qpe.Phase(numerator=9, denominator=8) + native_options = qpe.Options(precision=3, phase=phase) + assert phase.numerator == 1 + assert phase.denominator == 8 + assert native_options.phase == Fraction(1, 8) + assert_generates(benchmark) + + +def test_qpe_rejects_untyped_phase_input() -> None: + """Reject generic dictionaries at the typed Python boundary.""" + with pytest.raises(TypeError, match=r"fractions\.Fraction or Phase"): + qpe.Options( + precision=3, + phase={"numerator": 1, "denominator": 8}, # ty: ignore[invalid-argument-type] + ) + + +def test_qpe_normalizes_arbitrary_fraction() -> None: + """Normalize arbitrary-size fractions before entering the native type.""" + negative = qpe.Options(precision=3, phase=Fraction(-1, 8)) + large = qpe.Options(precision=3, phase=Fraction(2**80 + 1, 8)) + assert negative.phase == Fraction(7, 8) + assert large.phase == Fraction(1, 8) + + with pytest.raises(ValueError, match="denominator must fit in 64 bits"): + qpe.Options(precision=3, phase=Fraction(1, 2**80 + 1)) + + +def test_teleportation_reference_json_and_generation() -> None: + """Expose the fixed quantum teleportation benchmark without options.""" + benchmark = teleportation.Teleportation() + assert benchmark.output.name == "result" + assert benchmark.output.width == 1 + assert benchmark.probability("0") == 1 + assert benchmark.probability("1") == 0 + + evaluation = benchmark.evaluate({"0": 128}) + assert evaluation.total_variation_distance == pytest.approx(0) + assert evaluation.squared_hellinger_fidelity == pytest.approx(1) + assert evaluation.success_probability == 1 + assert json.loads(benchmark.instance_specification_json)["parameters"] == {} + + instance_copy = teleportation.Teleportation.from_instance_specification_json(benchmark.instance_specification_json) + manifest_copy = teleportation.Teleportation.from_manifest_json(benchmark.manifest_json) + assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id + + shots = 128 + counts = mlir.sample(benchmark.generate(), shots=shots, seed=17) + assert counts == {"0": shots} + assert_generates(benchmark) diff --git a/test/python/test_cli.py b/test/python/test_cli.py index ad91946f6c..2708494f17 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-quantum"' in ret.stdout assert '"qpe"' in ret.stdout assert '"teleportation"' in ret.stdout From ee7db7ef9dcb418c3673a02008e01ade56123ae9 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:39:35 +0200 Subject: [PATCH 02/24] Fix linter errors Assisted-by: GPT-5.6 Sol via Codex --- ...t_benchmark_generate_qft_adder_quantum.cpp | 29 ++++++++++--------- src/bench/QFTAdderQuantum.cpp | 1 + test/bench/test_qft_adder_quantum.cpp | 12 ++++++-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp index 4fc92d0dbf..ebeb3d49eb 100644 --- a/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp @@ -37,13 +37,20 @@ using namespace mlir; namespace { -void expectConstantIndex(Value value, int64_t expected) { +struct ControlledPhase { + qc::CtrlOp control; + qc::POp phase; +}; + +} // namespace + +static void expectConstantIndex(Value value, int64_t expected) { auto constant = value.getDefiningOp(); ASSERT_TRUE(constant); EXPECT_EQ(constant.value(), expected); } -void expectConstantFloat(Value value, double expected) { +static void expectConstantFloat(Value value, double expected) { auto constant = value.getDefiningOp(); ASSERT_TRUE(constant); auto attribute = dyn_cast(constant.getValue()); @@ -51,13 +58,13 @@ void expectConstantFloat(Value value, double expected) { EXPECT_DOUBLE_EQ(attribute.getValueAsDouble(), expected); } -void expectStaticLoop(scf::ForOp loop, int64_t lower, int64_t upper) { +static void expectStaticLoop(scf::ForOp loop, int64_t lower, int64_t upper) { expectConstantIndex(loop.getLowerBound(), lower); expectConstantIndex(loop.getUpperBound(), upper); expectConstantIndex(loop.getStep(), 1); } -[[nodiscard]] SmallVector topLevelLoops(ModuleOp moduleOp) { +[[nodiscard]] static SmallVector topLevelLoops(ModuleOp moduleOp) { SmallVector loops; moduleOp.walk([&](scf::ForOp loop) { if (!loop->getParentOfType()) { @@ -67,7 +74,7 @@ void expectStaticLoop(scf::ForOp loop, int64_t lower, int64_t upper) { return loops; } -[[nodiscard]] SmallVector nestedLoops(scf::ForOp outer) { +[[nodiscard]] static SmallVector nestedLoops(scf::ForOp outer) { SmallVector loops; outer.walk([&](scf::ForOp loop) { if (loop != outer) { @@ -77,7 +84,8 @@ void expectStaticLoop(scf::ForOp loop, int64_t lower, int64_t upper) { return loops; } -void expectAngleRecurrence(scf::ForOp loop, Value initialAngle, double factor) { +static void expectAngleRecurrence(scf::ForOp loop, Value initialAngle, + double factor) { ASSERT_EQ(loop.getInitArgs().size(), 1U); EXPECT_EQ(loop.getInitArgs().front(), initialAngle); auto angle = loop.getRegionIterArg(0); @@ -91,12 +99,7 @@ void expectAngleRecurrence(scf::ForOp loop, Value initialAngle, double factor) { expectConstantFloat(scale.getRhs(), factor); } -struct ControlledPhase { - qc::CtrlOp control; - qc::POp phase; -}; - -[[nodiscard]] ControlledPhase controlledPhase(scf::ForOp loop) { +[[nodiscard]] static ControlledPhase controlledPhase(scf::ForOp loop) { ControlledPhase result; size_t controls = 0; size_t phases = 0; @@ -117,8 +120,6 @@ struct ControlledPhase { return result; } -} // namespace - TEST(GenerateProgramTest, EmitsExactQuantumQFTAdderSchedule) { constexpr int64_t qubits = 3; auto program = generate(QFTAdderQuantum{{.qubits = qubits}}); diff --git a/src/bench/QFTAdderQuantum.cpp b/src/bench/QFTAdderQuantum.cpp index e3c3f76554..f50940ed8a 100644 --- a/src/bench/QFTAdderQuantum.cpp +++ b/src/bench/QFTAdderQuantum.cpp @@ -14,6 +14,7 @@ #include "bench/Evaluation.hpp" #include +#include #include #include diff --git a/test/bench/test_qft_adder_quantum.cpp b/test/bench/test_qft_adder_quantum.cpp index 7bd57737e6..3affe292f0 100644 --- a/test/bench/test_qft_adder_quantum.cpp +++ b/test/bench/test_qft_adder_quantum.cpp @@ -40,8 +40,16 @@ TEST(QFTAdderQuantum, ValidatesTheConfiguredInstance) { TEST(QFTAdderQuantum, GivesUniformWeightToCorrelatedSums) { const QFTAdderQuantum benchmark{{.qubits = 3}}; - for (const auto* outcome : {"000001", "001010", "010011", "011100", "100101", - "101110", "110111", "111000"}) { + for (const auto* outcome : { + "000001", + "001010", + "010011", + "011100", + "100101", + "101110", + "110111", + "111000", + }) { EXPECT_DOUBLE_EQ(benchmark.probability(outcome), 1. / 8.); } From 76b1185f8918fc291773431dd6dc6584ff6dee3d Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:58:58 +0200 Subject: [PATCH 03/24] Update changelog Assisted-by: GPT-5.6 Sol via Codex --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From df9e745d49fd3bf3b282c44adcc60fbf1d6a94e4 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:27:14 +0200 Subject: [PATCH 04/24] Refine quantum-input QFT adder Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/qft-adder-quantum.md | 33 +- bindings/bench/register_bench.cpp | 3 +- bindings/bench/register_qft_adder_quantum.cpp | 4 +- docs/benchmarks.md | 14 - mlir/bench/programs/CMakeLists.txt | 2 +- mlir/bench/programs/QFT.cpp | 42 +-- mlir/bench/programs/QFTAdderQuantum.cpp | 29 +- .../{QFTAdderUtils.cpp => QFTUtils.cpp} | 52 ++- .../programs/{QFTAdderUtils.h => QFTUtils.h} | 10 +- mlir/bench/programs/QPE.cpp | 38 +- ...t_benchmark_generate_qft_adder_quantum.cpp | 335 +++--------------- python/mqt/core/bench/qft_adder_quantum.pyi | 7 +- 12 files changed, 133 insertions(+), 436 deletions(-) rename mlir/bench/programs/{QFTAdderUtils.cpp => QFTUtils.cpp} (60%) rename mlir/bench/programs/{QFTAdderUtils.h => QFTUtils.h} (65%) diff --git a/.agent/plans/qft-adder-quantum.md b/.agent/plans/qft-adder-quantum.md index e59ae2964c..fccac4a0e5 100644 --- a/.agent/plans/qft-adder-quantum.md +++ b/.agent/plans/qft-adder-quantum.md @@ -1,7 +1,6 @@ # Add a quantum-input QFT adder benchmark -Status: in progress. The implementation is validated. The draft pull request and -its changelog reference remain to be added. +Status: complete. ## Goal and scope @@ -26,25 +25,25 @@ Register index zero is the least-significant bit. The forward QFT uses no swaps and visits targets from most to least significant. For target `t`, it applies H and then `CP(pi / 2^(t-c))` from every lower control `c`. The addition block applies the same controlled-phase gate from source control `c <= t` to -accumulator target `t`, including each `CP(pi)` gate. The inverse QFT reverses -the complete gate order and negates each phase. `CP` cannot be replaced with a -controlled RZ because their relative phases differ. +accumulator target `t`, including each `CP(pi)` gate. The inverse QFT visits +targets from least to most significant. It starts each target at `-pi / 2` and +halves the angle while visiting lower controls from nearest to farthest. This +order gives the exact inverse because the controlled-phase gates commute. It +also prevents distant rotations from making later nearby rotations underflow. +`CP` cannot be replaced with a controlled RZ because their relative phases +differ. The width is limited to 1024 qubits per register. This keeps the smallest required binary phase and the ideal probability representable as `double`. The implementation does not add swaps, carry qubits, approximate rotations, or an -alternative QFT convention. A private MLIR helper may own the shared forward and -inverse no-swap transforms; it must not change the existing QFT benchmark. - -## Work remaining - -- [ ] Create the draft stacked pull request and fold its number into the - existing unreleased structured-benchmark changelog entry. +alternative QFT convention. Private MLIR helpers own the shared phase loop and +the forward and inverse no-swap transforms. The standard QFT and QPE generators +use the same transforms. ## Validation -The release build, all 50 native benchmark tests, all 15 MLIR benchmark tests, -the benchmark CLI test, and 23 focused Python benchmark and CLI tests pass. The -Python test samples the width-three circuit and compares the result with the -analytic correlation. Stub generation, the general repository lint session, and -`git diff --check` pass. The separate C++ lint session was not run. +The focused MLIR test checks the controlled-addition register and phase +relations. The shared benchmark test checks QC and jeff generation. The Python +test samples the width-three circuit and compares the result with the analytic +correlation. The largest supported instance stays structured and uses finite +angles. diff --git a/bindings/bench/register_bench.cpp b/bindings/bench/register_bench.cpp index 6e4ee3cd9c..a9d7c4ebdb 100644 --- a/bindings/bench/register_bench.cpp +++ b/bindings/bench/register_bench.cpp @@ -71,7 +71,8 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { registerQFT(qft); const nb::module_ qftAdderQuantum = m.def_submodule( - "qft_adder_quantum", "Quantum-input QFT adder instances and options."); + "qft_adder_quantum", + "Quantum-input QFT adder benchmark instances and options."); registerQFTAdderQuantum(qftAdderQuantum); const nb::module_ qpe = diff --git a/bindings/bench/register_qft_adder_quantum.cpp b/bindings/bench/register_qft_adder_quantum.cpp index f8a86a0db4..38684d550f 100644 --- a/bindings/bench/register_qft_adder_quantum.cpp +++ b/bindings/bench/register_qft_adder_quantum.cpp @@ -32,7 +32,9 @@ void registerQFTAdderQuantum(const nb::module_& m) { "The number of qubits in each input register."); auto qftAdder = nb::class_( - m, "QFTAdderQuantum", "A validated quantum-input QFT adder benchmark."); + m, "QFTAdderQuantum", + "A validated quantum-input QFT adder benchmark.\n\n" + "Reference: https://arxiv.org/abs/quant-ph/0008033"); qftAdder.def(nb::init(), "options"_a) .def_prop_ro("options", &bench::QFTAdderQuantum::options, nb::rv_policy::reference_internal, diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 61e6e2c79d..9634dfffbe 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -53,20 +53,6 @@ print("Width:", benchmark.output.width) Each family validates its instance when it creates one. Fixed families need no options. -## Quantum-input QFT adder - -The `qft-adder-quantum` family implements Draper's -[QFT adder](https://arxiv.org/abs/quant-ph/0008033). For a configured width `n`, -the benchmark prepares an `n`-qubit addend register in the uniform -superposition and an `n`-qubit accumulator in state |1>. It applies the exact -no-swap QFT to the accumulator, the complete controlled-phase addition, and the -inverse QFT. - -The `2n`-bit result is the big-endian concatenation `addend || sum`. An outcome -has probability `2^-n` when `sum = addend + 1 mod 2^n` and probability zero -otherwise. Keeping both registers in the result exposes the correlation that -defines the addition; the sum alone would be uniform. - ## Inspect the canonical instance specification and manifest A canonical instance specification records every resolved default. A manifest diff --git a/mlir/bench/programs/CMakeLists.txt b/mlir/bench/programs/CMakeLists.txt index 53d84f4147..4688b50485 100644 --- a/mlir/bench/programs/CMakeLists.txt +++ b/mlir/bench/programs/CMakeLists.txt @@ -14,7 +14,7 @@ add_library( Multiplexer.cpp QFT.cpp QFTAdderQuantum.cpp - QFTAdderUtils.cpp + QFTUtils.cpp QPE.cpp Teleportation.cpp) target_link_libraries(MQTBenchmarkPrograms PUBLIC MQT::CoreBench MLIRQCProgramBuilder diff --git a/mlir/bench/programs/QFT.cpp b/mlir/bench/programs/QFT.cpp index da9dfddc32..3a7172e609 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,14 +59,16 @@ 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) { if (preparePlus) { builder.h(query); } auto previous = arith::SubIOp::create(builder, step, one); - qftPhaseRotationLoop( - builder, zero, step, std::numbers::pi / 2.0, 0.5, + detail::phaseRotationLoop( + builder, zero, step, one, firstAngle, half, [&](Value angle, Value distance) { auto bit = arith::SubIOp::create(builder, previous, distance); builder.scfIf(result, bit, [&] { builder.p(angle, query); }); diff --git a/mlir/bench/programs/QFTAdderQuantum.cpp b/mlir/bench/programs/QFTAdderQuantum.cpp index aeca0c6301..751b95c04d 100644 --- a/mlir/bench/programs/QFTAdderQuantum.cpp +++ b/mlir/bench/programs/QFTAdderQuantum.cpp @@ -11,14 +11,11 @@ #include "bench/QFTAdderQuantum.hpp" #include "Programs.h" -#include "QFTAdderUtils.h" +#include "QFTUtils.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include -#include -#include #include -#include #include #include @@ -33,23 +30,19 @@ static void addQuantumRegister(qc::QCProgramBuilder& builder, Value addend, 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(); - auto firstAngle = builder.floatConstant(std::numbers::pi); - auto half = builder.floatConstant(0.5); - auto loop = - scf::ForOp::create(builder, zero, upper, one, ValueRange{firstAngle}); - OpBuilder::InsertionGuard guard(builder); - builder.setInsertionPointToStart(loop.getBody()); - auto angle = loop.getRegionIterArg(0); - auto control = - arith::SubIOp::create(builder, target, loop.getInductionVar()) - .getResult(); - builder.cp(angle, builder.loadQubit(addend, control), - builder.loadQubit(sum, target)); - auto next = arith::MulFOp::create(builder, angle, half).getResult(); - scf::YieldOp::create(builder, ValueRange{next}); + 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)); + }); }); } diff --git a/mlir/bench/programs/QFTAdderUtils.cpp b/mlir/bench/programs/QFTUtils.cpp similarity index 60% rename from mlir/bench/programs/QFTAdderUtils.cpp rename to mlir/bench/programs/QFTUtils.cpp index 5e80c66fee..f034a82d34 100644 --- a/mlir/bench/programs/QFTAdderUtils.cpp +++ b/mlir/bench/programs/QFTUtils.cpp @@ -8,7 +8,7 @@ * Licensed under the MIT License */ -#include "QFTAdderUtils.h" +#include "QFTUtils.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" @@ -25,15 +25,12 @@ namespace mqt::bench::detail { using namespace mlir; -static void -phaseRotationLoop(qc::QCProgramBuilder& builder, Value upper, - Value initialAngle, double factor, - const function_ref& body) { - auto zero = builder.indexConstant(0); - auto one = builder.indexConstant(1); - auto scale = builder.floatConstant(factor); +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, zero, upper, one, ValueRange{initialAngle}); + scf::ForOp::create(builder, lower, upper, step, ValueRange{initialAngle}); OpBuilder::InsertionGuard guard(builder); builder.setInsertionPointToStart(loop.getBody()); auto angle = loop.getRegionIterArg(0); @@ -44,16 +41,19 @@ phaseRotationLoop(qc::QCProgramBuilder& builder, Value upper, 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(); - auto firstAngle = builder.floatConstant(std::numbers::pi / 2.); phaseRotationLoop( - builder, target, firstAngle, 0.5, [&](Value angle, Value distance) { + 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), @@ -67,24 +67,20 @@ void inverseQFT(qc::QCProgramBuilder& builder, Value qubitRegister, auto zero = builder.indexConstant(0); auto one = builder.indexConstant(1); auto upper = builder.indexConstant(qubits); - auto firstAngle = builder.floatConstant(-std::numbers::pi); + auto firstAngle = builder.floatConstant(-std::numbers::pi / 2.); auto half = builder.floatConstant(0.5); - auto loop = - scf::ForOp::create(builder, zero, upper, one, ValueRange{firstAngle}); - OpBuilder::InsertionGuard guard(builder); - builder.setInsertionPointToStart(loop.getBody()); - - auto target = loop.getInductionVar(); - auto initialAngle = loop.getRegionIterArg(0); - phaseRotationLoop( - builder, target, initialAngle, 2., [&](Value angle, Value control) { - builder.cp(angle, builder.loadQubit(qubitRegister, control), - builder.loadQubit(qubitRegister, target)); - }); - builder.h(builder.loadQubit(qubitRegister, target)); - - auto next = arith::MulFOp::create(builder, initialAngle, half).getResult(); - scf::YieldOp::create(builder, ValueRange{next}); + 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/QFTAdderUtils.h b/mlir/bench/programs/QFTUtils.h similarity index 65% rename from mlir/bench/programs/QFTAdderUtils.h rename to mlir/bench/programs/QFTUtils.h index 107eed1034..e9d26e2eb0 100644 --- a/mlir/bench/programs/QFTAdderUtils.h +++ b/mlir/bench/programs/QFTUtils.h @@ -10,6 +10,8 @@ #pragma once +#include + #include namespace mlir { @@ -22,7 +24,13 @@ class QCProgramBuilder; namespace mqt::bench::detail { -/// Apply the exact no-swap QFT used by the QFT-adder families. +/// 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); diff --git a/mlir/bench/programs/QPE.cpp b/mlir/bench/programs/QPE.cpp index b18a010435..3ecb1e896d 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,6 +72,8 @@ 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); @@ -98,8 +83,8 @@ iterativeQPE(qc::QCProgramBuilder& builder, const QPE& benchmark) { builder.cp(angle, query, ancilla); auto previous = arith::SubIOp::create(builder, step, one); - qpePhaseRotationLoop( - builder, lower, step, -std::numbers::pi / 2.0, 0.5, + detail::phaseRotationLoop( + builder, lower, step, one, firstCorrection, half, [&](Value correction, Value distance) { auto bit = arith::SubIOp::create(builder, previous, distance); builder.scfIf(result, bit, [&] { builder.p(correction, query); }); @@ -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/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp index ebeb3d49eb..6e466de469 100644 --- a/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp @@ -15,35 +15,22 @@ #include "mlir/bench/Generate.h" #include -#include #include #include #include #include #include -#include #include #include #include -#include #include #include -#include namespace mqt::bench { using namespace mlir; -namespace { - -struct ControlledPhase { - qc::CtrlOp control; - qc::POp phase; -}; - -} // namespace - static void expectConstantIndex(Value value, int64_t expected) { auto constant = value.getDefiningOp(); ASSERT_TRUE(constant); @@ -58,69 +45,7 @@ static void expectConstantFloat(Value value, double expected) { EXPECT_DOUBLE_EQ(attribute.getValueAsDouble(), expected); } -static void expectStaticLoop(scf::ForOp loop, int64_t lower, int64_t upper) { - expectConstantIndex(loop.getLowerBound(), lower); - expectConstantIndex(loop.getUpperBound(), upper); - expectConstantIndex(loop.getStep(), 1); -} - -[[nodiscard]] static SmallVector topLevelLoops(ModuleOp moduleOp) { - SmallVector loops; - moduleOp.walk([&](scf::ForOp loop) { - if (!loop->getParentOfType()) { - loops.push_back(loop); - } - }); - return loops; -} - -[[nodiscard]] static SmallVector nestedLoops(scf::ForOp outer) { - SmallVector loops; - outer.walk([&](scf::ForOp loop) { - if (loop != outer) { - loops.push_back(loop); - } - }); - return loops; -} - -static void expectAngleRecurrence(scf::ForOp loop, Value initialAngle, - double factor) { - ASSERT_EQ(loop.getInitArgs().size(), 1U); - EXPECT_EQ(loop.getInitArgs().front(), initialAngle); - auto angle = loop.getRegionIterArg(0); - - auto yield = dyn_cast(loop.getBody()->getTerminator()); - ASSERT_TRUE(yield); - ASSERT_EQ(yield.getNumOperands(), 1U); - auto scale = yield.getOperand(0).getDefiningOp(); - ASSERT_TRUE(scale); - EXPECT_EQ(scale.getLhs(), angle); - expectConstantFloat(scale.getRhs(), factor); -} - -[[nodiscard]] static ControlledPhase controlledPhase(scf::ForOp loop) { - ControlledPhase result; - size_t controls = 0; - size_t phases = 0; - loop.walk([&](qc::CtrlOp op) { - result.control = op; - ++controls; - }); - loop.walk([&](qc::POp op) { - result.phase = op; - ++phases; - }); - EXPECT_EQ(controls, 1U); - EXPECT_EQ(phases, 1U); - if (result.control) { - EXPECT_EQ(result.control.getNumControls(), 1U); - EXPECT_EQ(result.control.getNumTargets(), 1U); - } - return result; -} - -TEST(GenerateProgramTest, EmitsExactQuantumQFTAdderSchedule) { +TEST(GenerateProgramTest, EmitsQuantumQFTAdderCircuit) { constexpr int64_t qubits = 3; auto program = generate(QFTAdderQuantum{{.qubits = qubits}}); ASSERT_TRUE(program); @@ -134,223 +59,62 @@ TEST(GenerateProgramTest, EmitsExactQuantumQFTAdderSchedule) { EXPECT_EQ(test::countOps(moduleOp), 3U); EXPECT_EQ(test::countOps(moduleOp), 2U); EXPECT_EQ(test::countOps(moduleOp), 0U); - EXPECT_EQ(test::countOps(moduleOp), 0U); - EXPECT_EQ(test::countOps(moduleOp), 9U); - size_t unitaries = 0; - moduleOp.walk([&](qc::UnitaryOpInterface /*unused*/) { ++unitaries; }); - EXPECT_EQ(unitaries, 10U); - - cbit::AllocOp resultAllocation; - moduleOp.walk([&](cbit::AllocOp op) { resultAllocation = op; }); - ASSERT_TRUE(resultAllocation); - EXPECT_EQ(resultAllocation.getResult().getType().getWidth(), 2 * qubits); - auto loops = topLevelLoops(moduleOp); - ASSERT_EQ(loops.size(), 6U); - for (auto loop : loops) { - expectStaticLoop(loop, 0, qubits); - } - - qc::HOp prepareAddend; - loops[0].walk([&](qc::HOp op) { prepareAddend = op; }); - ASSERT_TRUE(prepareAddend); - auto addendLoad = prepareAddend.getQubit(0).getDefiningOp(); - ASSERT_TRUE(addendLoad); - EXPECT_EQ(addendLoad.getIndices().front(), loops[0].getInductionVar()); - auto addend = addendLoad.getMemref(); + // 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); - qc::XOp prepareOne; - moduleOp.walk([&](qc::XOp op) { prepareOne = op; }); - ASSERT_TRUE(prepareOne); - auto sumLoad = prepareOne.getQubit(0).getDefiningOp(); - ASSERT_TRUE(sumLoad); - expectConstantIndex(sumLoad.getIndices().front(), 0); - auto sum = sumLoad.getMemref(); - EXPECT_NE(addend, sum); - EXPECT_TRUE(loops[0]->isBeforeInBlock(prepareOne)); - EXPECT_TRUE(prepareOne->isBeforeInBlock(loops[1])); + auto sourceLoad = addition.getControl(0).getDefiningOp(); + auto targetLoad = addition.getTarget(0).getDefiningOp(); + ASSERT_TRUE(sourceLoad); + ASSERT_TRUE(targetLoad); - // The positive, no-swap QFT visits t = n - 1 ... 0. For each t, it - // applies H(y[t]) before CP(pi / 2^(t-c), y[c], y[t]) for c = t - 1 ... 0. - auto forward = loops[1]; - qc::HOp forwardH; - forward.walk([&](qc::HOp op) { forwardH = op; }); - ASSERT_TRUE(forwardH); - auto forwardTargetLoad = forwardH.getQubit(0).getDefiningOp(); - ASSERT_TRUE(forwardTargetLoad); - EXPECT_EQ(forwardTargetLoad.getMemref(), sum); - auto forwardTarget = forwardTargetLoad.getIndices().front(); - auto forwardTargetExpression = forwardTarget.getDefiningOp(); - ASSERT_TRUE(forwardTargetExpression); - expectConstantIndex(forwardTargetExpression.getLhs(), qubits - 1); - EXPECT_EQ(forwardTargetExpression.getRhs(), forward.getInductionVar()); + auto inner = addition->getParentOfType(); + ASSERT_TRUE(inner); + auto outer = inner->getParentOfType(); + ASSERT_TRUE(outer); - auto forwardInnerLoops = nestedLoops(forward); - ASSERT_EQ(forwardInnerLoops.size(), 1U); - auto forwardInner = forwardInnerLoops.front(); - EXPECT_TRUE(forwardH->isBeforeInBlock(forwardInner)); - expectConstantIndex(forwardInner.getLowerBound(), 0); - EXPECT_EQ(forwardInner.getUpperBound(), forwardTarget); - expectConstantIndex(forwardInner.getStep(), 1); - ASSERT_EQ(forwardInner.getInitArgs().size(), 1U); - expectConstantFloat(forwardInner.getInitArgs().front(), - std::numbers::pi / 2.); - expectAngleRecurrence(forwardInner, forwardInner.getInitArgs().front(), 0.5); - auto forwardPhase = controlledPhase(forwardInner); - ASSERT_TRUE(forwardPhase.control); - ASSERT_TRUE(forwardPhase.phase); - EXPECT_EQ(forwardPhase.phase.getTheta(), forwardInner.getRegionIterArg(0)); - auto forwardControl = - forwardPhase.control.getControl(0).getDefiningOp(); - auto forwardPhaseTarget = - forwardPhase.control.getTarget(0).getDefiningOp(); - ASSERT_TRUE(forwardControl); - ASSERT_TRUE(forwardPhaseTarget); - EXPECT_EQ(forwardControl.getMemref(), sum); - EXPECT_EQ(forwardPhaseTarget.getMemref(), sum); - EXPECT_EQ(forwardPhaseTarget.getIndices().front(), forwardTarget); - auto forwardControlExpression = - forwardControl.getIndices().front().getDefiningOp(); - ASSERT_TRUE(forwardControlExpression); - EXPECT_EQ(forwardControlExpression.getRhs(), forwardInner.getInductionVar()); - auto previous = - forwardControlExpression.getLhs().getDefiningOp(); - ASSERT_TRUE(previous); - expectConstantIndex(previous.getLhs(), qubits - 2); - EXPECT_EQ(previous.getRhs(), forward.getInductionVar()); + auto target = targetLoad.getIndices().front(); + auto targetIndex = target.getDefiningOp(); + ASSERT_TRUE(targetIndex); + expectConstantIndex(targetIndex.getLhs(), qubits - 1); + EXPECT_EQ(targetIndex.getRhs(), outer.getInductionVar()); - // Draper's addition visits t = n - 1 ... 0 and c = t ... 0. The first - // gate at each target is CP(pi), including the matching source bit. - auto addition = loops[2]; - EXPECT_TRUE(forward->isBeforeInBlock(addition)); - auto additionInnerLoops = nestedLoops(addition); - ASSERT_EQ(additionInnerLoops.size(), 1U); - auto additionInner = additionInnerLoops.front(); - expectConstantIndex(additionInner.getLowerBound(), 0); - auto additionUpper = - additionInner.getUpperBound().getDefiningOp(); - ASSERT_TRUE(additionUpper); - expectConstantIndex(additionUpper.getLhs(), qubits); - EXPECT_EQ(additionUpper.getRhs(), addition.getInductionVar()); - expectConstantIndex(additionInner.getStep(), 1); - ASSERT_EQ(additionInner.getInitArgs().size(), 1U); - expectConstantFloat(additionInner.getInitArgs().front(), std::numbers::pi); - expectAngleRecurrence(additionInner, additionInner.getInitArgs().front(), - 0.5); - auto additionPhase = controlledPhase(additionInner); - ASSERT_TRUE(additionPhase.control); - ASSERT_TRUE(additionPhase.phase); - EXPECT_EQ(additionPhase.phase.getTheta(), additionInner.getRegionIterArg(0)); - auto sourceControl = - additionPhase.control.getControl(0).getDefiningOp(); - auto sumTarget = - additionPhase.control.getTarget(0).getDefiningOp(); - ASSERT_TRUE(sourceControl); - ASSERT_TRUE(sumTarget); - auto additionTarget = sumTarget.getIndices().front(); - auto descendingAdditionTarget = additionTarget.getDefiningOp(); - ASSERT_TRUE(descendingAdditionTarget); - expectConstantIndex(descendingAdditionTarget.getLhs(), qubits - 1); - EXPECT_EQ(descendingAdditionTarget.getRhs(), addition.getInductionVar()); - EXPECT_EQ(sourceControl.getMemref(), addend); - EXPECT_EQ(sumTarget.getMemref(), sum); - EXPECT_EQ(sumTarget.getIndices().front(), additionTarget); auto sourceIndex = - sourceControl.getIndices().front().getDefiningOp(); + sourceLoad.getIndices().front().getDefiningOp(); ASSERT_TRUE(sourceIndex); - EXPECT_EQ(sourceIndex.getLhs(), additionTarget); - EXPECT_EQ(sourceIndex.getRhs(), additionInner.getInductionVar()); - - // The inverse reverses every QFT gate: c = 0 ... t - 1, then H(y[t]), - // while its first inner-loop angle progresses as -pi / 2^t. - auto inverse = loops[3]; - EXPECT_TRUE(addition->isBeforeInBlock(inverse)); - ASSERT_EQ(inverse.getInitArgs().size(), 1U); - expectConstantFloat(inverse.getInitArgs().front(), -std::numbers::pi); - auto inverseInnerLoops = nestedLoops(inverse); - ASSERT_EQ(inverseInnerLoops.size(), 1U); - auto inverseInner = inverseInnerLoops.front(); - expectConstantIndex(inverseInner.getLowerBound(), 0); - EXPECT_EQ(inverseInner.getUpperBound(), inverse.getInductionVar()); - expectConstantIndex(inverseInner.getStep(), 1); - expectAngleRecurrence(inverseInner, inverse.getRegionIterArg(0), 2.); - auto inversePhase = controlledPhase(inverseInner); - ASSERT_TRUE(inversePhase.control); - ASSERT_TRUE(inversePhase.phase); - EXPECT_EQ(inversePhase.phase.getTheta(), inverseInner.getRegionIterArg(0)); - auto inverseControl = - inversePhase.control.getControl(0).getDefiningOp(); - auto inverseTarget = - inversePhase.control.getTarget(0).getDefiningOp(); - ASSERT_TRUE(inverseControl); - ASSERT_TRUE(inverseTarget); - EXPECT_EQ(inverseControl.getMemref(), sum); - EXPECT_EQ(inverseControl.getIndices().front(), - inverseInner.getInductionVar()); - EXPECT_EQ(inverseTarget.getMemref(), sum); - EXPECT_EQ(inverseTarget.getIndices().front(), inverse.getInductionVar()); - qc::HOp inverseH; - inverse.walk([&](qc::HOp op) { inverseH = op; }); - ASSERT_TRUE(inverseH); - EXPECT_TRUE(inverseInner->isBeforeInBlock(inverseH)); - auto inverseHLoad = inverseH.getQubit(0).getDefiningOp(); - ASSERT_TRUE(inverseHLoad); - EXPECT_EQ(inverseHLoad.getMemref(), sum); - EXPECT_EQ(inverseHLoad.getIndices().front(), inverse.getInductionVar()); - auto inverseYield = - dyn_cast(inverse.getBody()->getTerminator()); - ASSERT_TRUE(inverseYield); - ASSERT_EQ(inverseYield.getNumOperands(), 1U); - auto nextInverseAngle = - inverseYield.getOperand(0).getDefiningOp(); - ASSERT_TRUE(nextInverseAngle); - EXPECT_EQ(nextInverseAngle.getLhs(), inverse.getRegionIterArg(0)); - expectConstantFloat(nextInverseAngle.getRhs(), 0.5); - - // Register bit zero is least significant. Measuring sum at i and addend at - // n + i makes the displayed big-endian result `addend || sum`. - auto sumMeasurementLoop = loops[4]; - auto addendMeasurementLoop = loops[5]; - EXPECT_TRUE(inverse->isBeforeInBlock(sumMeasurementLoop)); - EXPECT_TRUE(sumMeasurementLoop->isBeforeInBlock(addendMeasurementLoop)); - qc::MeasureOp sumMeasurement; - sumMeasurementLoop.walk([&](qc::MeasureOp op) { sumMeasurement = op; }); - qc::MeasureOp addendMeasurement; - addendMeasurementLoop.walk([&](qc::MeasureOp op) { addendMeasurement = op; }); - ASSERT_TRUE(sumMeasurement); - ASSERT_TRUE(addendMeasurement); - auto measuredSum = sumMeasurement.getQubit().getDefiningOp(); - auto measuredAddend = - addendMeasurement.getQubit().getDefiningOp(); - ASSERT_TRUE(measuredSum); - ASSERT_TRUE(measuredAddend); - EXPECT_EQ(measuredSum.getMemref(), sum); - EXPECT_EQ(measuredSum.getIndices().front(), - sumMeasurementLoop.getInductionVar()); - EXPECT_EQ(measuredAddend.getMemref(), addend); - EXPECT_EQ(measuredAddend.getIndices().front(), - addendMeasurementLoop.getInductionVar()); + 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 sumStore = - dyn_cast(*sumMeasurement.getResult().user_begin()); - auto addendStore = - dyn_cast(*addendMeasurement.getResult().user_begin()); - ASSERT_TRUE(sumStore); - ASSERT_TRUE(addendStore); - EXPECT_EQ(sumStore.getReg(), resultAllocation.getResult()); - EXPECT_EQ(sumStore.getIndex(), sumMeasurementLoop.getInductionVar()); - EXPECT_EQ(addendStore.getReg(), resultAllocation.getResult()); - auto displayedAddendIndex = - addendStore.getIndex().getDefiningOp(); - ASSERT_TRUE(displayedAddendIndex); - if (displayedAddendIndex.getLhs() == - addendMeasurementLoop.getInductionVar()) { - expectConstantIndex(displayedAddendIndex.getRhs(), qubits); - } else { - expectConstantIndex(displayedAddendIndex.getLhs(), qubits); - EXPECT_EQ(displayedAddendIndex.getRhs(), - addendMeasurementLoop.getInductionVar()); - } + 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) { @@ -365,7 +129,6 @@ TEST(GenerateProgramTest, KeepsLargestQuantumQFTAdderFiniteAndStructured) { EXPECT_TRUE(std::isfinite(value.getValueAsDouble())); } }); - test::expectJeffRoundTrip(std::move(*program)); } } // namespace mqt::bench diff --git a/python/mqt/core/bench/qft_adder_quantum.pyi b/python/mqt/core/bench/qft_adder_quantum.pyi index fcc0c01dd1..dbd6973555 100644 --- a/python/mqt/core/bench/qft_adder_quantum.pyi +++ b/python/mqt/core/bench/qft_adder_quantum.pyi @@ -6,7 +6,7 @@ # # Licensed under the MIT License -"""Quantum-input QFT adder instances and options.""" +"""Quantum-input QFT adder benchmark instances and options.""" from collections.abc import Mapping @@ -22,7 +22,10 @@ class Options: """The number of qubits in each input register.""" class QFTAdderQuantum: - """A validated quantum-input QFT adder benchmark.""" + """A validated quantum-input QFT adder benchmark. + + Reference: https://arxiv.org/abs/quant-ph/0008033 + """ def __init__(self, options: Options) -> None: ... @property From 3e62dba9043bd4ae4db60e33350326e1b513fa00 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 12:13:22 +0000 Subject: [PATCH 05/24] =?UTF-8?q?=F0=9F=A7=AA=20Check=20shared=20QFT=20ben?= =?UTF-8?q?chmarks=20with=20DD=20sampling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the quantum adder's fixed input and modular result contract. Reuse register measurement and compare both QFT and QPE methods with their analytic references. Unroll QPE's small test circuits to fold the phase table into scalar values supported by the DD interpreter. Assisted-by: GPT-6 via Codex --- bindings/bench/register_qft_adder_quantum.cpp | 6 +++++- include/mqt-core/bench/QFTAdderQuantum.hpp | 4 +++- mlir/bench/programs/QFTAdderQuantum.cpp | 4 +--- python/mqt/core/bench/qft_adder_quantum.pyi | 4 +++- test/python/test_bench.py | 18 ++++++++++++++++++ 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/bindings/bench/register_qft_adder_quantum.cpp b/bindings/bench/register_qft_adder_quantum.cpp index 38684d550f..2d1ab8eb9c 100644 --- a/bindings/bench/register_qft_adder_quantum.cpp +++ b/bindings/bench/register_qft_adder_quantum.cpp @@ -33,7 +33,11 @@ void registerQFTAdderQuantum(const nb::module_& m) { auto qftAdder = nb::class_( m, "QFTAdderQuantum", - "A validated quantum-input QFT adder benchmark.\n\n" + "A QFT adder with an n-qubit addend in |+>^n and an accumulator " + "in |1>.\n\n" + "Big-endian outcomes concatenate the addend and sum, each with n " + "bits. The sum is addend + 1 modulo 2^n; each valid outcome has " + "probability 2^-n.\n\n" "Reference: https://arxiv.org/abs/quant-ph/0008033"); qftAdder.def(nb::init(), "options"_a) .def_prop_ro("options", &bench::QFTAdderQuantum::options, diff --git a/include/mqt-core/bench/QFTAdderQuantum.hpp b/include/mqt-core/bench/QFTAdderQuantum.hpp index 6f9b894dc0..215fa3694d 100644 --- a/include/mqt-core/bench/QFTAdderQuantum.hpp +++ b/include/mqt-core/bench/QFTAdderQuantum.hpp @@ -26,7 +26,9 @@ struct QFTAdderQuantumOptions { size_t qubits; }; -/// A validated quantum-input QFT adder and its analytic reference. +/// A QFT adder with an n-qubit addend in |+>^n and an accumulator in |1>. +/// Big-endian outcomes concatenate the addend and sum, each with n bits. +/// The sum is addend + 1 modulo 2^n; each valid outcome has probability 2^-n. class MQT_CORE_BENCH_EXPORT QFTAdderQuantum final { public: explicit QFTAdderQuantum(QFTAdderQuantumOptions options); diff --git a/mlir/bench/programs/QFTAdderQuantum.cpp b/mlir/bench/programs/QFTAdderQuantum.cpp index 751b95c04d..d9476ebd88 100644 --- a/mlir/bench/programs/QFTAdderQuantum.cpp +++ b/mlir/bench/programs/QFTAdderQuantum.cpp @@ -64,9 +64,7 @@ SmallVector qftAdderQuantum(qc::QCProgramBuilder& builder, addQuantumRegister(builder, addend, sum, qubits); detail::inverseQFT(builder, sum, qubits); - builder.scfFor(0, qubits, 1, [&](Value index) { - builder.measure(builder.loadQubit(sum, index), result, index); - }); + builder.measureQubitRegister(sum, result, qubits); auto resultOffset = builder.indexConstant(qubits); builder.scfFor(0, qubits, 1, [&](Value index) { auto resultIndex = diff --git a/python/mqt/core/bench/qft_adder_quantum.pyi b/python/mqt/core/bench/qft_adder_quantum.pyi index dbd6973555..c3c44f02d2 100644 --- a/python/mqt/core/bench/qft_adder_quantum.pyi +++ b/python/mqt/core/bench/qft_adder_quantum.pyi @@ -22,7 +22,9 @@ class Options: """The number of qubits in each input register.""" class QFTAdderQuantum: - """A validated quantum-input QFT adder benchmark. + """A QFT adder with an n-qubit addend in |+>^n and an accumulator in |1>. + + Big-endian outcomes concatenate the addend and sum, each with n bits. The sum is addend + 1 modulo 2^n; each valid outcome has probability 2^-n. Reference: https://arxiv.org/abs/quant-ph/0008033 """ diff --git a/test/python/test_bench.py b/test/python/test_bench.py index d8f0247958..e33807394c 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -133,6 +133,10 @@ def test_qft_methods_share_the_periodic_reference() -> None: assert ( qft.QFT.from_instance_specification_json(benchmark.instance_specification_json).case_id == benchmark.case_id ) + shots = 16_384 + counts = benchmark.generate().to_qco().sample(shots=shots, seed=17) + assert sum(counts.values()) == shots + assert benchmark.evaluate(counts).total_variation_distance < 0.03 assert_generates(benchmark) @@ -197,6 +201,20 @@ def test_qpe_accepts_fraction_and_native_phase() -> None: assert_generates(benchmark) +@pytest.mark.parametrize("method", [qpe.Method.STANDARD, qpe.Method.ITERATIVE]) +@pytest.mark.parametrize("phase", [Fraction(3, 8), Fraction(1, 3)]) +def test_qpe_dd_sampling_matches_reference(method: qpe.Method, phase: Fraction) -> None: + """Execute exact and inexact phases with both inverse-QFT implementations.""" + benchmark = qpe.QPE(qpe.Options(precision=3, phase=phase, method=method)) + shots = 16_384 + program = benchmark.generate().to_qco() + # Fold phase-table reads to scalars supported by the DD interpreter. + program.unroll_quantum_loops() + counts = program.sample(shots=shots, seed=17) + assert sum(counts.values()) == shots + assert benchmark.evaluate(counts).total_variation_distance < 0.03 + + def test_qpe_rejects_untyped_phase_input() -> None: """Reject generic dictionaries at the typed Python boundary.""" with pytest.raises(TypeError, match=r"fractions\.Fraction or Phase"): From 2d41f8f23cdc0e7ae6a494d1ef7d08d44001d95d Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:24:14 +0200 Subject: [PATCH 06/24] Fix linter errors Assisted-by: GPT-5.6 Sol via Codex --- mlir/bench/programs/QFT.cpp | 8 ++++---- mlir/bench/programs/QPE.cpp | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mlir/bench/programs/QFT.cpp b/mlir/bench/programs/QFT.cpp index 3a7172e609..6e4deec2d0 100644 --- a/mlir/bench/programs/QFT.cpp +++ b/mlir/bench/programs/QFT.cpp @@ -62,19 +62,19 @@ semiclassicalQFT(qc::QCProgramBuilder& builder, const QFT& benchmark) { 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); + auto previous = arith::SubIOp::create(builder, index, one); detail::phaseRotationLoop( - builder, zero, step, one, firstAngle, half, + 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/QPE.cpp b/mlir/bench/programs/QPE.cpp index 3ecb1e896d..78993f226e 100644 --- a/mlir/bench/programs/QPE.cpp +++ b/mlir/bench/programs/QPE.cpp @@ -75,23 +75,23 @@ iterativeQPE(qc::QCProgramBuilder& builder, const QPE& 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); + auto previous = arith::SubIOp::create(builder, index, one); detail::phaseRotationLoop( - builder, lower, step, one, firstCorrection, half, + 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}; From 63710abdb9745e7d02d5f84e45150bfeca460b66 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:34:42 +0200 Subject: [PATCH 07/24] Improve QFT adder docstring Assisted-by: GPT-5.6 Sol via Codex --- bindings/bench/register_qft_adder_quantum.cpp | 12 ++++++------ python/mqt/core/bench/qft_adder_quantum.pyi | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bindings/bench/register_qft_adder_quantum.cpp b/bindings/bench/register_qft_adder_quantum.cpp index 2d1ab8eb9c..75049acacb 100644 --- a/bindings/bench/register_qft_adder_quantum.cpp +++ b/bindings/bench/register_qft_adder_quantum.cpp @@ -33,12 +33,12 @@ void registerQFTAdderQuantum(const nb::module_& m) { auto qftAdder = nb::class_( m, "QFTAdderQuantum", - "A QFT adder with an n-qubit addend in |+>^n and an accumulator " - "in |1>.\n\n" - "Big-endian outcomes concatenate the addend and sum, each with n " - "bits. The sum is addend + 1 modulo 2^n; each valid outcome has " - "probability 2^-n.\n\n" - "Reference: https://arxiv.org/abs/quant-ph/0008033"); + R"pb(A QFT adder with an n-qubit addend in :math:`|+\\rangle^{\\otimes n}` and an accumulator in :math:`|1\\rangle`. + +Big-endian outcomes concatenate the addend and sum, each with n bits. The sum +is addend + 1 modulo 2^n; each valid outcome has probability 2^-n. + +Reference: https://arxiv.org/abs/quant-ph/0008033)pb"); qftAdder.def(nb::init(), "options"_a) .def_prop_ro("options", &bench::QFTAdderQuantum::options, nb::rv_policy::reference_internal, diff --git a/python/mqt/core/bench/qft_adder_quantum.pyi b/python/mqt/core/bench/qft_adder_quantum.pyi index c3c44f02d2..6eeda2181d 100644 --- a/python/mqt/core/bench/qft_adder_quantum.pyi +++ b/python/mqt/core/bench/qft_adder_quantum.pyi @@ -22,9 +22,10 @@ class Options: """The number of qubits in each input register.""" class QFTAdderQuantum: - """A QFT adder with an n-qubit addend in |+>^n and an accumulator in |1>. + """A QFT adder with an n-qubit addend in :math:`|+\\\\rangle^{\\\\otimes n}` and an accumulator in :math:`|1\\\\rangle`. - Big-endian outcomes concatenate the addend and sum, each with n bits. The sum is addend + 1 modulo 2^n; each valid outcome has probability 2^-n. + Big-endian outcomes concatenate the addend and sum, each with n bits. The sum + is addend + 1 modulo 2^n; each valid outcome has probability 2^-n. Reference: https://arxiv.org/abs/quant-ph/0008033 """ From 5fcdb65bc663750c38f1ad52c65223692574fc6f Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 12:50:03 +0000 Subject: [PATCH 08/24] =?UTF-8?q?=E2=9C=A8=20Read=20dense=20phase=20tables?= =?UTF-8?q?=20in=20DD=20simulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support dense rank-one f64 constants and bounds-checked tensor.extract through the shared DD interpreter. Reuse attribute storage and preserve the scalar arithmetic and argument-binding contracts. Check loop-indexed and splat tables, invalid indices, and unsupported tensor constants. Run QPE benchmark sampling without loop unrolling. Assisted-by: GPT-6 via Codex --- test/python/test_bench.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/python/test_bench.py b/test/python/test_bench.py index e33807394c..4bc1971bc8 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -207,10 +207,7 @@ def test_qpe_dd_sampling_matches_reference(method: qpe.Method, phase: Fraction) """Execute exact and inexact phases with both inverse-QFT implementations.""" benchmark = qpe.QPE(qpe.Options(precision=3, phase=phase, method=method)) shots = 16_384 - program = benchmark.generate().to_qco() - # Fold phase-table reads to scalars supported by the DD interpreter. - program.unroll_quantum_loops() - counts = program.sample(shots=shots, seed=17) + counts = benchmark.generate().to_qco().sample(shots=shots, seed=17) assert sum(counts.values()) == shots assert benchmark.evaluate(counts).total_variation_distance < 0.03 From 3fa3b59225031a5aacdab6a03c3e356be6f51d15 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:14:57 +0200 Subject: [PATCH 09/24] Fix quantum QFT adder docstring markup Assisted-by: GPT-5.6 Sol via Codex --- bindings/bench/register_qft_adder_quantum.cpp | 7 ++++--- python/mqt/core/bench/qft_adder_quantum.pyi | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bindings/bench/register_qft_adder_quantum.cpp b/bindings/bench/register_qft_adder_quantum.cpp index 75049acacb..84ea7800db 100644 --- a/bindings/bench/register_qft_adder_quantum.cpp +++ b/bindings/bench/register_qft_adder_quantum.cpp @@ -33,10 +33,11 @@ void registerQFTAdderQuantum(const nb::module_& m) { auto qftAdder = nb::class_( m, "QFTAdderQuantum", - R"pb(A QFT adder with an n-qubit addend in :math:`|+\\rangle^{\\otimes n}` and an accumulator in :math:`|1\\rangle`. + R"pb(A QFT adder with an :math:`n`-qubit addend in :math:`|+\rangle^{\otimes n}` and an accumulator in :math:`|1\rangle`. -Big-endian outcomes concatenate the addend and sum, each with n bits. The sum -is addend + 1 modulo 2^n; each valid outcome has probability 2^-n. +Big-endian outcomes concatenate the addend and sum, each with :math:`n` bits. +For addend :math:`a`, the sum is :math:`(a + 1) \bmod 2^n`; each valid +outcome has probability :math:`2^{-n}`. Reference: https://arxiv.org/abs/quant-ph/0008033)pb"); qftAdder.def(nb::init(), "options"_a) diff --git a/python/mqt/core/bench/qft_adder_quantum.pyi b/python/mqt/core/bench/qft_adder_quantum.pyi index 6eeda2181d..9ce6d9264e 100644 --- a/python/mqt/core/bench/qft_adder_quantum.pyi +++ b/python/mqt/core/bench/qft_adder_quantum.pyi @@ -22,10 +22,11 @@ class Options: """The number of qubits in each input register.""" class QFTAdderQuantum: - """A QFT adder with an n-qubit addend in :math:`|+\\\\rangle^{\\\\otimes n}` and an accumulator in :math:`|1\\\\rangle`. + """A QFT adder with an :math:`n`-qubit addend in :math:`|+\\rangle^{\\otimes n}` and an accumulator in :math:`|1\\rangle`. - Big-endian outcomes concatenate the addend and sum, each with n bits. The sum - is addend + 1 modulo 2^n; each valid outcome has probability 2^-n. + Big-endian outcomes concatenate the addend and sum, each with :math:`n` bits. + For addend :math:`a`, the sum is :math:`(a + 1) \\bmod 2^n`; each valid + outcome has probability :math:`2^{-n}`. Reference: https://arxiv.org/abs/quant-ph/0008033 """ From c07bdf36210e6b8736f8b3bf3189ee98522e1dea Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 13:38:15 +0000 Subject: [PATCH 10/24] =?UTF-8?q?=F0=9F=93=9D=20Compact=20adder=20plan=20a?= =?UTF-8?q?nd=20use=20public=20sampling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the supported input and validation limits without an implementation log. Assisted-by: GPT-6 via Codex --- .agent/plans/qft-adder-quantum.md | 63 ++++++++++++------------------- test/python/test_bench.py | 6 +-- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/.agent/plans/qft-adder-quantum.md b/.agent/plans/qft-adder-quantum.md index fccac4a0e5..4482bc52ab 100644 --- a/.agent/plans/qft-adder-quantum.md +++ b/.agent/plans/qft-adder-quantum.md @@ -1,49 +1,34 @@ -# Add a quantum-input QFT adder benchmark +# Register-input QFT adder benchmark -Status: complete. +Status: complete; configurable inputs and family consolidation remain proposals. ## Goal and scope -Add the `qft-adder-quantum` structured benchmark from Draper's -[Addition on a Quantum Computer](https://arxiv.org/abs/quant-ph/0008033). The -benchmark must be available through the typed C++, JSON, command-line, Python, -and MLIR generation interfaces. It must generate the full no-swap QFT, Draper -addition, and inverse-QFT circuit rather than a circuit with the same output -distribution. - -The benchmark parameter is the width `n` of each quantum register. The source -register is prepared as |+>^n and the accumulator as |1>. The one logical -`result` output has width `2n` and is written as the big-endian concatenation -`addend || sum`. Its ideal distribution has probability `2^-n` exactly when -`sum = addend + 1 mod 2^n`. Measuring both registers keeps this correlation -observable; measuring the sum alone would produce an uninformative uniform -distribution. +Expose `qft-adder-quantum` through C++, Python, JSON, the CLI, and MLIR. +`src/bench/QFTAdderQuantum.cpp` owns its analytic reference; +`mlir/bench/programs/QFTAdderQuantum.cpp` emits Draper's controlled-phase adder. +The addend starts in `|+>^n`, the accumulator in `|1>`, and the result is the +big-endian concatenation `addend || sum`, with `sum = addend + 1 mod 2^n`. ## Decisions -Register index zero is the least-significant bit. The forward QFT uses no swaps -and visits targets from most to least significant. For target `t`, it applies H -and then `CP(pi / 2^(t-c))` from every lower control `c`. The addition block -applies the same controlled-phase gate from source control `c <= t` to -accumulator target `t`, including each `CP(pi)` gate. The inverse QFT visits -targets from least to most significant. It starts each target at `-pi / 2` and -halves the angle while visiting lower controls from nearest to farthest. This -order gives the exact inverse because the controlled-phase gates commute. It -also prevents distant rotations from making later nearby rotations underflow. -`CP` cannot be replaced with a controlled RZ because their relative phases -differ. - -The width is limited to 1024 qubits per register. This keeps the smallest -required binary phase and the ideal probability representable as `double`. The -implementation does not add swaps, carry qubits, approximate rotations, or an -alternative QFT convention. Private MLIR helpers own the shared phase loop and -the forward and inverse no-swap transforms. The standard QFT and QPE generators -use the same transforms. +Measure both registers: the sum alone is uniform and cannot check addition. The +shared no-swap QFT helpers in `mlir/bench/programs/QFTUtils.*` also serve QFT +and QPE. Keep controlled phase gates; controlled RZ changes relative phases. +Halve angles from the largest rotation to avoid premature underflow. The +1024-qubit register limit keeps the reference probability representable. + +The DD interpreter reads dense rank-one f64 phase tables with checked indices; +QPE and adder tests can sample structured programs without loop unrolling. The +name distinguishes a register-held addend from a classical constant, not quantum +computation from classical computation. Configurable operands and a shared +family need an agreed overflow contract before changing the public API. ## Validation -The focused MLIR test checks the controlled-addition register and phase -relations. The shared benchmark test checks QC and jeff generation. The Python -test samples the width-three circuit and compares the result with the analytic -correlation. The largest supported instance stays structured and uses finite -angles. +Run `mqt-core-bench-test` and `mqt-core-mlir-unittests-benchmark` from their +build directories, and `uv run --no-sync pytest test/python/test_bench.py`. +Prior local checks passed for reference/JSON behavior, QC/jeff generation, phase +structure, and DD sampling of the width-three correlated distribution. Sampling +this fixed input does not certify arbitrary accumulators or relative phases; +those require broader inputs and statevector or functionality checks. diff --git a/test/python/test_bench.py b/test/python/test_bench.py index 4bc1971bc8..73f36a9085 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -134,7 +134,7 @@ def test_qft_methods_share_the_periodic_reference() -> None: qft.QFT.from_instance_specification_json(benchmark.instance_specification_json).case_id == benchmark.case_id ) shots = 16_384 - counts = benchmark.generate().to_qco().sample(shots=shots, seed=17) + counts = mlir.sample(benchmark.generate(), shots=shots, seed=17) assert sum(counts.values()) == shots assert benchmark.evaluate(counts).total_variation_distance < 0.03 assert_generates(benchmark) @@ -165,7 +165,7 @@ def test_quantum_qft_adder_reference_json_and_generation() -> None: sampled = qft_adder_quantum.QFTAdderQuantum(qft_adder_quantum.Options(qubits=3)) shots = 16_384 - counts = sampled.generate().to_qco().sample(shots=shots, seed=17) + counts = mlir.sample(sampled.generate(), shots=shots, seed=17) assert sum(counts.values()) == shots assert sampled.evaluate(counts).total_variation_distance < 0.03 assert_generates(benchmark) @@ -207,7 +207,7 @@ def test_qpe_dd_sampling_matches_reference(method: qpe.Method, phase: Fraction) """Execute exact and inexact phases with both inverse-QFT implementations.""" benchmark = qpe.QPE(qpe.Options(precision=3, phase=phase, method=method)) shots = 16_384 - counts = benchmark.generate().to_qco().sample(shots=shots, seed=17) + counts = mlir.sample(benchmark.generate(), shots=shots, seed=17) assert sum(counts.values()) == shots assert benchmark.evaluate(counts).total_variation_distance < 0.03 From 93f9d64b38148d51e441f46320f1ebd73233871f Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:55:52 +0200 Subject: [PATCH 11/24] Add classical-input QFT adder benchmark Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/qft-adder-classical.md | 48 ++++ bindings/bench/CMakeLists.txt | 1 + bindings/bench/register_bench.cpp | 6 + .../bench/register_qft_adder_classical.cpp | 89 ++++++++ docs/benchmarks.md | 12 + include/mqt-core/bench/BenchmarkFamilies.inc | 2 + include/mqt-core/bench/JSON.hpp | 1 + include/mqt-core/bench/QFTAdderClassical.hpp | 50 +++++ mlir/bench/programs/CMakeLists.txt | 1 + mlir/bench/programs/Programs.h | 5 + mlir/bench/programs/QFTAdderClassical.cpp | 70 ++++++ mlir/include/mlir/bench/Generate.h | 5 + mlir/unittests/bench/CMakeLists.txt | 1 + mlir/unittests/bench/QFTAdderTestUtils.h | 208 ++++++++++++++++++ mlir/unittests/bench/test_benchmark_cli.cmake | 4 +- .../bench/test_benchmark_generate.cpp | 2 + ...benchmark_generate_qft_adder_classical.cpp | 192 ++++++++++++++++ python/mqt/core/bench/__init__.pyi | 1 + python/mqt/core/bench/qft_adder_classical.pyi | 67 ++++++ src/bench/JSON.cpp | 53 +++++ src/bench/QFTAdderClassical.cpp | 75 +++++++ test/bench/test_json.cpp | 61 ++++- test/bench/test_qft_adder_classical.cpp | 77 +++++++ test/python/test_bench.py | 41 +++- test/python/test_cli.py | 1 + 25 files changed, 1069 insertions(+), 4 deletions(-) create mode 100644 .agent/plans/qft-adder-classical.md create mode 100644 bindings/bench/register_qft_adder_classical.cpp create mode 100644 include/mqt-core/bench/QFTAdderClassical.hpp create mode 100644 mlir/bench/programs/QFTAdderClassical.cpp create mode 100644 mlir/unittests/bench/QFTAdderTestUtils.h create mode 100644 mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp create mode 100644 python/mqt/core/bench/qft_adder_classical.pyi create mode 100644 src/bench/QFTAdderClassical.cpp create mode 100644 test/bench/test_qft_adder_classical.cpp diff --git a/.agent/plans/qft-adder-classical.md b/.agent/plans/qft-adder-classical.md new file mode 100644 index 0000000000..d27076c7f3 --- /dev/null +++ b/.agent/plans/qft-adder-classical.md @@ -0,0 +1,48 @@ +# Add a classical-input QFT adder benchmark + +Status: implementation complete. The stacked draft pull request and changelog +reference remain to be completed. + +## Goal and scope + +Add the `qft-adder-classical` structured benchmark from Beauregard's +[Circuit for Shor's algorithm using 2n+3 qubits](https://arxiv.org/abs/quant-ph/0205095), +Figure 3. Expose the benchmark through the typed C++, JSON, command-line, +Python, and MLIR generation interfaces. + +The instance parameter is a nonempty big-endian classical addend. Its length +defines an `n`-bit value. The benchmark prepares an `n+1`-qubit accumulator as +`|1>`, applies Beauregard's exact classical Fourier addition, and measures one +big-endian `result` output. The exact reference is the zero-extended addend plus +one; the extra accumulator qubit preserves overflow. + +## Decisions + +The generator uses the shared exact no-swap QFT and inverse-QFT helpers in +`mlir/bench/programs/QFTAdderUtils.*`. Between them, it emits exactly one +unconditional phase gate for each accumulator wire, including a zero-angle gate. +For little-endian accumulator wire `j`, the phase is the binary fraction formed +by addend bits `j` through zero. The extra wire receives the continued fraction +and therefore records carry. + +Compute the phase table by scanning the addend from least to most significant: +divide the previous angle by two and add pi for a set bit. Append one more +halved angle for the overflow wire. This produces canonical angles in +`[0, 2*pi)` without converting an arbitrary-width addend to a fixed-width +integer. The input length is limited to 1023 so the accumulator and QFT remain +within 1024 qubits. + +The fixed `|1>` accumulator is the benchmark harness, not part of the source's +general adder definition. Do not add swaps, approximate rotations, a carry +ancilla, or controlled phases: Beauregard's classical-input optimization +combines each wire's classically known rotations into one single-qubit phase. + +## Work completed + +- [x] Add the typed family, strict JSON contract, binding, stubs, and reference + tests. +- [x] Generate and structurally test the complete Figure 3 circuit. +- [x] Document the source, harness, bit order, phase convention, and output. +- [x] Validate the focused native, MLIR, and Python behavior. +- [ ] Create the draft pull request on the quantum-input QFT-adder branch and + add its number to the rolling structured-benchmark changelog entry. diff --git a/bindings/bench/CMakeLists.txt b/bindings/bench/CMakeLists.txt index 2027ed3d12..e504b2c894 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_classical.cpp register_qft_adder_quantum.cpp register_qpe.cpp register_teleportation.cpp) diff --git a/bindings/bench/register_bench.cpp b/bindings/bench/register_bench.cpp index a9d7c4ebdb..03aa034e7b 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 registerQFTAdderClassical(const nb::module_& m); void registerQFTAdderQuantum(const nb::module_& m); void registerQPE(const nb::module_& m); void registerTeleportation(const nb::module_& m); @@ -70,6 +71,11 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { m.def_submodule("qft", "QFT benchmark instances and options."); registerQFT(qft); + const nb::module_ qftAdderClassical = + m.def_submodule("qft_adder_classical", + "Classical-input QFT adder instances and options."); + registerQFTAdderClassical(qftAdderClassical); + const nb::module_ qftAdderQuantum = m.def_submodule( "qft_adder_quantum", "Quantum-input QFT adder benchmark instances and options."); diff --git a/bindings/bench/register_qft_adder_classical.cpp b/bindings/bench/register_qft_adder_classical.cpp new file mode 100644 index 0000000000..0aec277cf4 --- /dev/null +++ b/bindings/bench/register_qft_adder_classical.cpp @@ -0,0 +1,89 @@ +/* + * 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/QFTAdderClassical.hpp" + +#include +#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 registerQFTAdderClassical(const nb::module_& m) { + nb::class_( + m, "Options", "Parameters for a classical-input QFT adder benchmark.") + .def(nb::init(), nb::kw_only(), "addend"_a) + .def_ro("addend", &bench::QFTAdderClassicalOptions::addend, + "The big-endian classical addend."); + + auto qftAdder = nb::class_( + m, "QFTAdderClassical", + "A validated classical-input QFT adder benchmark."); + qftAdder.def(nb::init(), "options"_a) + .def_prop_ro("options", &bench::QFTAdderClassical::options, + nb::rv_policy::reference_internal, + "The resolved benchmark parameters.") + .def_prop_ro("output", &bench::QFTAdderClassical::output, + nb::rv_policy::reference_internal, + "The logical result register.") + .def_prop_ro("expected_result", &bench::QFTAdderClassical::expectedResult, + nb::rv_policy::reference_internal, + "The deterministic big-endian result.") + .def("probability", &bench::QFTAdderClassical::probability, "outcome"_a, + "Return the ideal probability of an outcome.") + .def("evaluate", &bench::QFTAdderClassical::evaluate, "counts"_a, + "Compare sampled counts with the ideal distribution.") + .def( + "generate", + [](const bench::QFTAdderClassical& 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::QFTAdderClassical& value) { + return bench::toInstanceSpecificationJSON(value); + }, + "The canonical instance specification JSON.") + .def_prop_ro( + "manifest_json", + [](const bench::QFTAdderClassical& value) { + return bench::toManifestJSON(value); + }, + "The canonical manifest JSON.") + .def_prop_ro( + "case_id", + [](const bench::QFTAdderClassical& value) { + return bench::caseId(value); + }, + "The stable semantic case ID.") + .def_static("from_instance_specification_json", + &bench::qftAdderClassicalFromInstanceSpecificationJSON, + "json"_a, nb::kw_only(), + "source"_a = "", + "Parse a strict benchmark instance specification.") + .def_static("from_manifest_json", + &bench::qftAdderClassicalFromManifestJSON, "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..c48bc5c2b6 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -53,6 +53,18 @@ print("Width:", benchmark.output.width) Each family validates its instance when it creates one. Fixed families need no options. +## Classical-input QFT adder + +The `qft-adder-classical` family implements the classical-input QFT adder from +[Beauregard's circuit for Shor's algorithm](https://arxiv.org/abs/quant-ph/0205095). +The `addend` parameter is a big-endian binary string. Leading zeros define the +input width. The benchmark prepares an accumulator in state |1>, applies the +exact no-swap QFT, one combined phase gate for each Fourier qubit, and the +inverse QFT. + +For an `n`-bit addend, the result has `n + 1` bits. The extra qubit retains the +carry, so the deterministic result is the zero-extended addend plus one. + ## Inspect the canonical instance specification and manifest A canonical instance specification records every resolved default. A manifest diff --git a/include/mqt-core/bench/BenchmarkFamilies.inc b/include/mqt-core/bench/BenchmarkFamilies.inc index c87149b77a..1eb6324530 100644 --- a/include/mqt-core/bench/BenchmarkFamilies.inc +++ b/include/mqt-core/bench/BenchmarkFamilies.inc @@ -30,6 +30,8 @@ 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(QFTAdderClassical, qftAdderClassical, + "qft-adder-classical", 1) MQT_BENCHMARK_FAMILY(QFTAdderQuantum, qftAdderQuantum, "qft-adder-quantum", 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 7d6e13016c..ae62643bce 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/QFTAdderClassical.hpp" #include "bench/QFTAdderQuantum.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" diff --git a/include/mqt-core/bench/QFTAdderClassical.hpp b/include/mqt-core/bench/QFTAdderClassical.hpp new file mode 100644 index 0000000000..ec4a7fa510 --- /dev/null +++ b/include/mqt-core/bench/QFTAdderClassical.hpp @@ -0,0 +1,50 @@ +/* + * 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 + +namespace mqt::bench { + +/// Parameters for one classical-input QFT adder benchmark instance. +struct QFTAdderClassicalOptions { + static constexpr size_t MAX_ADDEND_BITS = 1'023; + + /// Big-endian classical addend. Leading zeros define its width. + std::string addend; +}; + +/// A validated classical-input QFT adder and its analytic reference. +class MQT_CORE_BENCH_EXPORT QFTAdderClassical final { +public: + explicit QFTAdderClassical(QFTAdderClassicalOptions options); + + [[nodiscard]] const QFTAdderClassicalOptions& options() const noexcept; + [[nodiscard]] const Output& output() const noexcept; + /// Return the deterministic big-endian result. + [[nodiscard]] const std::string& expectedResult() const noexcept; + /// Return the ideal probability of a big-endian 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: + QFTAdderClassicalOptions options_; + Output output_; + std::string expectedResult_; +}; + +} // namespace mqt::bench diff --git a/mlir/bench/programs/CMakeLists.txt b/mlir/bench/programs/CMakeLists.txt index 4688b50485..7e89dcd76b 100644 --- a/mlir/bench/programs/CMakeLists.txt +++ b/mlir/bench/programs/CMakeLists.txt @@ -13,6 +13,7 @@ add_library( Grover.cpp Multiplexer.cpp QFT.cpp + QFTAdderClassical.cpp QFTAdderQuantum.cpp QFTUtils.cpp QPE.cpp diff --git a/mlir/bench/programs/Programs.h b/mlir/bench/programs/Programs.h index c50d98b453..d26b660241 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 QFTAdderClassical; class QFTAdderQuantum; class QPE; class Teleportation; @@ -49,6 +50,10 @@ SmallVector multiplexer(qc::QCProgramBuilder& builder, /// Emit one configured QFT benchmark. SmallVector qft(qc::QCProgramBuilder& builder, const QFT& benchmark); +/// Emit one configured classical-input QFT adder benchmark. +SmallVector qftAdderClassical(qc::QCProgramBuilder& builder, + const QFTAdderClassical& benchmark); + /// Emit one configured quantum-input QFT adder benchmark. SmallVector qftAdderQuantum(qc::QCProgramBuilder& builder, const QFTAdderQuantum& benchmark); diff --git a/mlir/bench/programs/QFTAdderClassical.cpp b/mlir/bench/programs/QFTAdderClassical.cpp new file mode 100644 index 0000000000..cc46747821 --- /dev/null +++ b/mlir/bench/programs/QFTAdderClassical.cpp @@ -0,0 +1,70 @@ +/* + * 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/QFTAdderClassical.hpp" + +#include "Programs.h" +#include "QFTAdderUtils.h" +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace mqt::bench { + +using namespace mlir; + +[[nodiscard]] static SmallVector +phaseAngles(const std::string_view addend) { + 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)); + } + angles.push_back(static_cast(angle / 2.L)); + return angles; +} + +SmallVector qftAdderClassical(qc::QCProgramBuilder& builder, + const QFTAdderClassical& benchmark) { + const auto qubits = + static_cast(benchmark.options().addend.size() + 1U); + auto sum = builder.allocQubitRegisterStorage(qubits, "sum"); + auto result = builder.allocClassicalBitRegister( + static_cast(benchmark.output().width), benchmark.output().name); + + auto zero = builder.indexConstant(0); + builder.x(builder.loadQubit(sum, zero)); + + detail::forwardQFT(builder, sum, qubits); + const auto angles = phaseAngles(benchmark.options().addend); + for (size_t target = 0; target < angles.size(); ++target) { + auto angle = builder.floatConstant(angles[target]); + auto index = builder.indexConstant(static_cast(target)); + builder.p(angle, builder.loadQubit(sum, index)); + } + detail::inverseQFT(builder, sum, qubits); + + builder.measureQubitRegister(sum, result, qubits); + return {result}; +} + +} // namespace mqt::bench diff --git a/mlir/include/mlir/bench/Generate.h b/mlir/include/mlir/bench/Generate.h index 32bdd1b1e0..8fa3e459f1 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 QFTAdderClassical; class QFTAdderQuantum; class QPE; class Teleportation; @@ -50,6 +51,10 @@ generate(const Multiplexer& benchmark); /// Generate a configured quantum Fourier-transform benchmark. [[nodiscard]] std::optional generate(const QFT& benchmark); +/// Generate a configured classical-input QFT adder benchmark. +[[nodiscard]] std::optional +generate(const QFTAdderClassical& benchmark); + /// Generate a configured quantum-input QFT adder benchmark. [[nodiscard]] std::optional generate(const QFTAdderQuantum& benchmark); diff --git a/mlir/unittests/bench/CMakeLists.txt b/mlir/unittests/bench/CMakeLists.txt index 2fb3e60e83..21381654cf 100644 --- a/mlir/unittests/bench/CMakeLists.txt +++ b/mlir/unittests/bench/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable( test_benchmark_generate_grover.cpp test_benchmark_generate_multiplexer.cpp test_benchmark_generate_qft.cpp + test_benchmark_generate_qft_adder_classical.cpp test_benchmark_generate_qft_adder_quantum.cpp test_benchmark_generate_qpe.cpp test_benchmark_generate_teleportation.cpp) diff --git a/mlir/unittests/bench/QFTAdderTestUtils.h b/mlir/unittests/bench/QFTAdderTestUtils.h new file mode 100644 index 0000000000..a9f2385901 --- /dev/null +++ b/mlir/unittests/bench/QFTAdderTestUtils.h @@ -0,0 +1,208 @@ +/* + * 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 "mlir/Dialect/QC/IR/QCOps.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mqt::bench::test { + +using namespace mlir; + +inline void expectConstantIndex(Value value, const int64_t expected) { + auto constant = value.getDefiningOp(); + ASSERT_TRUE(constant); + EXPECT_EQ(constant.value(), expected); +} + +inline void expectConstantFloat(Value value, const double expected) { + auto constant = value.getDefiningOp(); + ASSERT_TRUE(constant); + auto attribute = dyn_cast(constant.getValue()); + ASSERT_TRUE(attribute); + EXPECT_DOUBLE_EQ(attribute.getValueAsDouble(), expected); +} + +inline void expectStaticLoop(scf::ForOp loop, const int64_t lower, + const int64_t upper) { + expectConstantIndex(loop.getLowerBound(), lower); + expectConstantIndex(loop.getUpperBound(), upper); + expectConstantIndex(loop.getStep(), 1); +} + +[[nodiscard]] inline SmallVector topLevelLoops(ModuleOp moduleOp) { + SmallVector loops; + moduleOp.walk([&](scf::ForOp loop) { + if (!loop->getParentOfType()) { + loops.push_back(loop); + } + }); + return loops; +} + +[[nodiscard]] inline SmallVector nestedLoops(scf::ForOp outer) { + SmallVector loops; + outer.walk([&](scf::ForOp loop) { + if (loop != outer) { + loops.push_back(loop); + } + }); + return loops; +} + +inline void expectAngleRecurrence(scf::ForOp loop, Value initialAngle, + const double factor) { + ASSERT_EQ(loop.getInitArgs().size(), 1U); + EXPECT_EQ(loop.getInitArgs().front(), initialAngle); + auto angle = loop.getRegionIterArg(0); + + auto yield = dyn_cast(loop.getBody()->getTerminator()); + ASSERT_TRUE(yield); + ASSERT_EQ(yield.getNumOperands(), 1U); + auto scale = yield.getOperand(0).getDefiningOp(); + ASSERT_TRUE(scale); + EXPECT_EQ(scale.getLhs(), angle); + expectConstantFloat(scale.getRhs(), factor); +} + +struct ControlledPhase { + qc::CtrlOp control; + qc::POp phase; +}; + +[[nodiscard]] inline ControlledPhase controlledPhase(scf::ForOp loop) { + ControlledPhase result; + size_t controls = 0; + size_t phases = 0; + loop.walk([&](qc::CtrlOp op) { + result.control = op; + ++controls; + }); + loop.walk([&](qc::POp op) { + result.phase = op; + ++phases; + }); + EXPECT_EQ(controls, 1U); + EXPECT_EQ(phases, 1U); + if (result.control) { + EXPECT_EQ(result.control.getNumControls(), 1U); + EXPECT_EQ(result.control.getNumTargets(), 1U); + } + return result; +} + +inline void expectForwardQFT(scf::ForOp forward, Value qubitRegister, + const int64_t qubits) { + qc::HOp forwardH; + forward.walk([&](qc::HOp op) { forwardH = op; }); + ASSERT_TRUE(forwardH); + auto targetLoad = forwardH.getQubit(0).getDefiningOp(); + ASSERT_TRUE(targetLoad); + EXPECT_EQ(targetLoad.getMemref(), qubitRegister); + auto target = targetLoad.getIndices().front(); + auto targetExpression = target.getDefiningOp(); + ASSERT_TRUE(targetExpression); + expectConstantIndex(targetExpression.getLhs(), qubits - 1); + EXPECT_EQ(targetExpression.getRhs(), forward.getInductionVar()); + + auto innerLoops = nestedLoops(forward); + ASSERT_EQ(innerLoops.size(), 1U); + auto inner = innerLoops.front(); + EXPECT_TRUE(forwardH->isBeforeInBlock(inner)); + expectConstantIndex(inner.getLowerBound(), 0); + EXPECT_EQ(inner.getUpperBound(), target); + expectConstantIndex(inner.getStep(), 1); + ASSERT_EQ(inner.getInitArgs().size(), 1U); + expectConstantFloat(inner.getInitArgs().front(), std::numbers::pi / 2.); + expectAngleRecurrence(inner, inner.getInitArgs().front(), 0.5); + + auto controlled = controlledPhase(inner); + ASSERT_TRUE(controlled.control); + ASSERT_TRUE(controlled.phase); + EXPECT_EQ(controlled.phase.getTheta(), inner.getRegionIterArg(0)); + auto controlLoad = + controlled.control.getControl(0).getDefiningOp(); + auto phaseTargetLoad = + controlled.control.getTarget(0).getDefiningOp(); + ASSERT_TRUE(controlLoad); + ASSERT_TRUE(phaseTargetLoad); + EXPECT_EQ(controlLoad.getMemref(), qubitRegister); + EXPECT_EQ(phaseTargetLoad.getMemref(), qubitRegister); + EXPECT_EQ(phaseTargetLoad.getIndices().front(), target); + auto controlExpression = + controlLoad.getIndices().front().getDefiningOp(); + ASSERT_TRUE(controlExpression); + EXPECT_EQ(controlExpression.getRhs(), inner.getInductionVar()); + auto previous = controlExpression.getLhs().getDefiningOp(); + ASSERT_TRUE(previous); + expectConstantIndex(previous.getLhs(), qubits - 2); + EXPECT_EQ(previous.getRhs(), forward.getInductionVar()); +} + +inline void expectInverseQFT(scf::ForOp inverse, Value qubitRegister) { + ASSERT_EQ(inverse.getInitArgs().size(), 1U); + expectConstantFloat(inverse.getInitArgs().front(), -std::numbers::pi); + auto innerLoops = nestedLoops(inverse); + ASSERT_EQ(innerLoops.size(), 1U); + auto inner = innerLoops.front(); + expectConstantIndex(inner.getLowerBound(), 0); + EXPECT_EQ(inner.getUpperBound(), inverse.getInductionVar()); + expectConstantIndex(inner.getStep(), 1); + expectAngleRecurrence(inner, inverse.getRegionIterArg(0), 2.); + + auto controlled = controlledPhase(inner); + ASSERT_TRUE(controlled.control); + ASSERT_TRUE(controlled.phase); + EXPECT_EQ(controlled.phase.getTheta(), inner.getRegionIterArg(0)); + auto controlLoad = + controlled.control.getControl(0).getDefiningOp(); + auto targetLoad = + controlled.control.getTarget(0).getDefiningOp(); + ASSERT_TRUE(controlLoad); + ASSERT_TRUE(targetLoad); + EXPECT_EQ(controlLoad.getMemref(), qubitRegister); + EXPECT_EQ(controlLoad.getIndices().front(), inner.getInductionVar()); + EXPECT_EQ(targetLoad.getMemref(), qubitRegister); + EXPECT_EQ(targetLoad.getIndices().front(), inverse.getInductionVar()); + + qc::HOp inverseH; + inverse.walk([&](qc::HOp op) { inverseH = op; }); + ASSERT_TRUE(inverseH); + EXPECT_TRUE(inner->isBeforeInBlock(inverseH)); + auto hLoad = inverseH.getQubit(0).getDefiningOp(); + ASSERT_TRUE(hLoad); + EXPECT_EQ(hLoad.getMemref(), qubitRegister); + EXPECT_EQ(hLoad.getIndices().front(), inverse.getInductionVar()); + + auto yield = dyn_cast(inverse.getBody()->getTerminator()); + ASSERT_TRUE(yield); + ASSERT_EQ(yield.getNumOperands(), 1U); + auto nextAngle = yield.getOperand(0).getDefiningOp(); + ASSERT_TRUE(nextAngle); + EXPECT_EQ(nextAngle.getLhs(), inverse.getRegionIterArg(0)); + expectConstantFloat(nextAngle.getRhs(), 0.5); +} + +} // namespace mqt::bench::test diff --git a/mlir/unittests/bench/test_benchmark_cli.cmake b/mlir/unittests/bench/test_benchmark_cli.cmake index 888b5e2c5d..3844278d0e 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 8) - message(FATAL_ERROR "list returned ${benchmark_count} benchmarks instead of 8") +if(NOT benchmark_count EQUAL 9) + message(FATAL_ERROR "list returned ${benchmark_count} benchmarks instead of 9") 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 ee26cde8aa..00f9b44f24 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/QFTAdderClassical.hpp" #include "bench/QFTAdderQuantum.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -45,6 +46,7 @@ TEST(GenerateProgramTest, GeneratesEveryBenchmarkMethodAsQCAndJeff) { expectValidQCAndJeff(QFT{{.qubits = 3, .periodExponent = 1}}); expectValidQCAndJeff(QFT{ {.qubits = 3, .periodExponent = 1, .method = QFTMethod::Semiclassical}}); + expectValidQCAndJeff(QFTAdderClassical{{.addend = "101"}}); expectValidQCAndJeff(QFTAdderQuantum{{.qubits = 3}}); expectValidQCAndJeff(QPE{{.precision = 3, .phase = Phase(3, 8)}}); expectValidQCAndJeff(QPE{ diff --git a/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp new file mode 100644 index 0000000000..53a51596bd --- /dev/null +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp @@ -0,0 +1,192 @@ +/* + * 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 "QFTAdderTestUtils.h" +#include "TestUtils.h" +#include "bench/QFTAdderClassical.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 + +namespace mqt::bench { + +using namespace mlir; + +namespace { + +struct ClassicalAdderCase { + std::string addend; + std::vector angles; +}; + +class ClassicalQFTAdderStructureTest + : public testing::TestWithParam {}; + +} // namespace + +TEST_P(ClassicalQFTAdderStructureTest, EmitsExactClassicalQFTAdderSchedule) { + const auto& testCase = GetParam(); + const auto qubits = static_cast(testCase.addend.size() + 1U); + auto program = generate(QFTAdderClassical{{.addend = testCase.addend}}); + ASSERT_TRUE(program); + auto moduleOp = program->module(); + + EXPECT_EQ(test::countOps(moduleOp), 1U); + EXPECT_EQ(test::countOps(moduleOp), 1U); + EXPECT_EQ(test::countOps(moduleOp), 2U); + EXPECT_EQ(test::countOps(moduleOp), 1U); + EXPECT_EQ(test::countOps(moduleOp), 2U); + EXPECT_EQ(test::countOps(moduleOp), testCase.angles.size() + 2U); + EXPECT_EQ(test::countOps(moduleOp), 1U); + EXPECT_EQ(test::countOps(moduleOp), 0U); + EXPECT_EQ(test::countOps(moduleOp), 0U); + EXPECT_EQ(test::countOps(moduleOp), 5U); + + cbit::AllocOp resultAllocation; + moduleOp.walk([&](cbit::AllocOp op) { resultAllocation = op; }); + ASSERT_TRUE(resultAllocation); + EXPECT_EQ(resultAllocation.getResult().getType().getWidth(), qubits); + + auto loops = test::topLevelLoops(moduleOp); + ASSERT_EQ(loops.size(), 3U); + for (auto loop : loops) { + test::expectStaticLoop(loop, 0, qubits); + } + + qc::XOp prepareOne; + moduleOp.walk([&](qc::XOp op) { prepareOne = op; }); + ASSERT_TRUE(prepareOne); + auto oneLoad = prepareOne.getQubit(0).getDefiningOp(); + ASSERT_TRUE(oneLoad); + test::expectConstantIndex(oneLoad.getIndices().front(), 0); + auto sum = oneLoad.getMemref(); + + auto forward = loops[0]; + EXPECT_TRUE(prepareOne->isBeforeInBlock(forward)); + test::expectForwardQFT(forward, sum, qubits); + + // Beauregard's classical-input optimization combines all known rotations + // into one unconditional phase per wire, including the overflow wire. + SmallVector additionPhases; + moduleOp.walk([&](qc::POp op) { + if (!op->getParentOfType()) { + additionPhases.push_back(op); + } + }); + ASSERT_EQ(additionPhases.size(), testCase.angles.size()); + for (size_t target = 0; target < additionPhases.size(); ++target) { + auto phase = additionPhases[target]; + EXPECT_FALSE(phase->getParentOfType()); + test::expectConstantFloat(phase.getTheta(), testCase.angles[target]); + auto targetLoad = phase.getQubit(0).getDefiningOp(); + ASSERT_TRUE(targetLoad); + EXPECT_EQ(targetLoad.getMemref(), sum); + test::expectConstantIndex(targetLoad.getIndices().front(), + static_cast(target)); + EXPECT_TRUE(forward->isBeforeInBlock(phase)); + if (target != 0U) { + EXPECT_TRUE(additionPhases[target - 1]->isBeforeInBlock(phase)); + } + } + + auto inverse = loops[1]; + EXPECT_TRUE(additionPhases.back()->isBeforeInBlock(inverse)); + test::expectInverseQFT(inverse, sum); + + // Register bit zero is least significant. Equal source and destination + // indices therefore produce the declared big-endian result register. + auto measurementLoop = loops[2]; + EXPECT_TRUE(inverse->isBeforeInBlock(measurementLoop)); + qc::MeasureOp measurement; + measurementLoop.walk([&](qc::MeasureOp op) { measurement = op; }); + ASSERT_TRUE(measurement); + auto measured = measurement.getQubit().getDefiningOp(); + ASSERT_TRUE(measured); + EXPECT_EQ(measured.getMemref(), sum); + EXPECT_EQ(measured.getIndices().front(), measurementLoop.getInductionVar()); + auto store = dyn_cast(*measurement.getResult().user_begin()); + ASSERT_TRUE(store); + EXPECT_EQ(store.getReg(), resultAllocation.getResult()); + EXPECT_EQ(store.getIndex(), measurementLoop.getInductionVar()); +} + +TEST(GenerateProgramTest, KeepsLargestClassicalQFTAdderFiniteAndSerializable) { + auto addend = std::string(QFTAdderClassicalOptions::MAX_ADDEND_BITS, '1'); + auto program = generate(QFTAdderClassical{{.addend = std::move(addend)}}); + ASSERT_TRUE(program); + auto moduleOp = program->module(); + + EXPECT_EQ(test::countOps(moduleOp), + QFTAdderClassicalOptions::MAX_ADDEND_BITS + 3U); + const auto operations = test::countOperations(moduleOp); + EXPECT_GT(operations, QFTAdderClassicalOptions::MAX_ADDEND_BITS); + EXPECT_LT(operations, 5U * QFTAdderClassicalOptions::MAX_ADDEND_BITS); + moduleOp.walk([&](arith::ConstantOp op) { + if (const auto value = dyn_cast(op.getValue())) { + EXPECT_TRUE(std::isfinite(value.getValueAsDouble())); + } + }); + test::expectJeffRoundTrip(std::move(*program)); +} + +INSTANTIATE_TEST_SUITE_P( + ExactPhases, ClassicalQFTAdderStructureTest, + testing::Values(ClassicalAdderCase{"0", {0., 0.}}, + ClassicalAdderCase{ + "1", {std::numbers::pi, std::numbers::pi / 2.}}, + ClassicalAdderCase{"101", + { + std::numbers::pi, + std::numbers::pi / 2., + 5. * std::numbers::pi / 4., + 5. * std::numbers::pi / 8., + }}, + ClassicalAdderCase{"111", + { + std::numbers::pi, + 3. * std::numbers::pi / 2., + 7. * std::numbers::pi / 4., + 7. * std::numbers::pi / 8., + }}, + ClassicalAdderCase{"110", + { + 0., + std::numbers::pi, + 3. * std::numbers::pi / 2., + 3. * std::numbers::pi / 4., + }}, + ClassicalAdderCase{"001", + { + std::numbers::pi, + std::numbers::pi / 2., + std::numbers::pi / 4., + std::numbers::pi / 8., + }})); + +} // namespace mqt::bench diff --git a/python/mqt/core/bench/__init__.pyi b/python/mqt/core/bench/__init__.pyi index a687ed8291..02f56d2483 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_classical as qft_adder_classical from mqt.core.bench import qft_adder_quantum as qft_adder_quantum 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_classical.pyi b/python/mqt/core/bench/qft_adder_classical.pyi new file mode 100644 index 0000000000..0e07510f04 --- /dev/null +++ b/python/mqt/core/bench/qft_adder_classical.pyi @@ -0,0 +1,67 @@ +# 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 + +"""Classical-input QFT adder instances and options.""" + +from collections.abc import Mapping + +import mqt.core.bench +import mqt.core.mlir + +class Options: + """Parameters for a classical-input QFT adder benchmark.""" + + def __init__(self, *, addend: str) -> None: ... + @property + def addend(self) -> str: + """The big-endian classical addend.""" + +class QFTAdderClassical: + """A validated classical-input QFT adder benchmark.""" + + 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 result register.""" + + @property + def expected_result(self) -> str: + """The deterministic big-endian result.""" + + 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 = "") -> QFTAdderClassical: + """Parse a strict benchmark instance specification.""" + + @staticmethod + def from_manifest_json(json: str, *, source: str = "") -> QFTAdderClassical: + """Parse a strict benchmark manifest.""" diff --git a/src/bench/JSON.cpp b/src/bench/JSON.cpp index 3c03659cfd..a228eccf2b 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/QFTAdderClassical.hpp" #include "bench/QFTAdderQuantum.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -424,6 +425,21 @@ parseMultiplexerParameters(const Json& parameters, } } +[[nodiscard]] QFTAdderClassical +parseQFTAdderClassicalParameters(const Json& parameters, + const std::string_view source) { + rejectUnknownKeys(parameters, {"addend"}, source, "$/parameters"); + try { + return QFTAdderClassical({ + .addend = + stringValue(required(parameters, "addend", source, "$/parameters"), + source, "$/parameters/addend"), + }); + } catch (const std::invalid_argument& error) { + fail(source, "$/parameters", error.what()); + } +} + [[nodiscard]] QFTAdderQuantum parseQFTAdderQuantumParameters(const Json& parameters, const std::string_view source) { @@ -543,6 +559,10 @@ parseTeleportationParameters(const Json& parameters, }; } +[[nodiscard]] Json parametersJSON(const QFTAdderClassical& benchmark) { + return {{"addend", benchmark.options().addend}}; +} + [[nodiscard]] Json parametersJSON(const QFTAdderQuantum& benchmark) { return {{"qubits", benchmark.options().qubits}}; } @@ -618,6 +638,17 @@ parseTeleportationParameters(const Json& parameters, }; } +[[nodiscard]] Json referenceJSON(const QFTAdderClassical& benchmark) { + return { + {"kind", "analytic"}, + {"model", "qft_adder_classical"}, + {"outcome_order", "big_endian"}, + {"output", benchmark.output().name}, + {"success_outcome", benchmark.expectedResult()}, + {"version", 1}, + }; +} + [[nodiscard]] Json referenceJSON(const QFTAdderQuantum& benchmark) { return { {"kind", "analytic"}, @@ -906,6 +937,28 @@ template }); } +[[nodiscard]] Json qftAdderClassicalInstanceSpecificationSchema() { + return baseInstanceSpecificationSchema({ + {"additionalProperties", false}, + { + "properties", + { + { + "addend", + { + {"maxLength", QFTAdderClassicalOptions::MAX_ADDEND_BITS}, + {"minLength", 1}, + {"pattern", "^[01]+$"}, + {"type", "string"}, + }, + }, + }, + }, + {"required", {"addend"}}, + {"type", "object"}, + }); +} + [[nodiscard]] Json qftAdderQuantumInstanceSpecificationSchema() { return baseInstanceSpecificationSchema({ {"additionalProperties", false}, diff --git a/src/bench/QFTAdderClassical.cpp b/src/bench/QFTAdderClassical.cpp new file mode 100644 index 0000000000..413add5856 --- /dev/null +++ b/src/bench/QFTAdderClassical.cpp @@ -0,0 +1,75 @@ +/* + * 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/QFTAdderClassical.hpp" + +#include "EvaluationUtils.hpp" +#include "bench/Evaluation.hpp" + +#include +#include +#include +#include +#include +#include + +namespace mqt::bench { + +[[nodiscard]] static std::string increment(const std::string_view addend) { + auto result = std::string{"0"} + std::string{addend}; + auto carry = true; + for (size_t index = result.size(); index > 0 && carry; --index) { + auto& bit = result[index - 1]; + carry = bit == '1'; + bit = carry ? '0' : '1'; + } + return result; +} + +QFTAdderClassical::QFTAdderClassical(QFTAdderClassicalOptions options) + : options_(std::move(options)), + output_{.name = "result", .width = options_.addend.size() + 1U} { + const auto width = options_.addend.size(); + if (width == 0 || width > QFTAdderClassicalOptions::MAX_ADDEND_BITS) { + throw std::invalid_argument( + "classical QFT adder addend must contain between 1 and 1023 bits"); + } + if (!std::ranges::all_of(options_.addend, [](const char bit) { + return bit == '0' || bit == '1'; + })) { + throw std::invalid_argument( + "classical QFT adder addend must contain only '0' and '1'"); + } + expectedResult_ = increment(options_.addend); +} + +const QFTAdderClassicalOptions& QFTAdderClassical::options() const noexcept { + return options_; +} + +const Output& QFTAdderClassical::output() const noexcept { return output_; } + +const std::string& QFTAdderClassical::expectedResult() const noexcept { + return expectedResult_; +} + +double QFTAdderClassical::probability(const std::string_view outcome) const { + detail::validateOutcome(outcome, output_.width); + return outcome == expectedResult_ ? 1. : 0.; +} + +Evaluation QFTAdderClassical::evaluate(const Counts& counts) const { + return detail::evaluate( + output_, counts, + [this](const std::string_view outcome) { return probability(outcome); }, + expectedResult_); +} + +} // namespace mqt::bench diff --git a/test/bench/test_json.cpp b/test/bench/test_json.cpp index f36de4160b..763d18e6c5 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/QFTAdderClassical.hpp" #include "bench/QFTAdderQuantum.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -57,6 +58,9 @@ using mqt::bench::multiplexerFromInstanceSpecificationJSON; using mqt::bench::multiplexerFromManifestJSON; using mqt::bench::Phase; using mqt::bench::QFT; +using mqt::bench::QFTAdderClassical; +using mqt::bench::qftAdderClassicalFromInstanceSpecificationJSON; +using mqt::bench::qftAdderClassicalFromManifestJSON; using mqt::bench::QFTAdderQuantum; using mqt::bench::qftAdderQuantumFromInstanceSpecificationJSON; using mqt::bench::qftAdderQuantumFromManifestJSON; @@ -123,6 +127,14 @@ TEST(BenchmarkJSON, toInstanceSpecificationJSON(qft), R"({"benchmark":"qft","parameters":{"method":"standard","period_exponent":2,"qubits":4},"schema_version":1})"); + const auto qftAdderClassical = qftAdderClassicalFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"qft-adder-classical","parameters":{"addend":"001"}})"); + EXPECT_EQ(qftAdderClassical.options().addend, "001"); + EXPECT_EQ(qftAdderClassical.expectedResult(), "0010"); + EXPECT_EQ( + toInstanceSpecificationJSON(qftAdderClassical), + R"({"benchmark":"qft-adder-classical","parameters":{"addend":"001"},"schema_version":1})"); + const auto qftAdderQuantum = qftAdderQuantumFromInstanceSpecificationJSON( R"({"schema_version":1,"benchmark":"qft-adder-quantum","parameters":{"qubits":3}})"); EXPECT_EQ(qftAdderQuantum.options().qubits, 3); @@ -153,6 +165,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { const Multiplexer multiplexer{{.qubits = 7}}; const QFT qft{ {.qubits = 4, .periodExponent = 2, .method = QFTMethod::Semiclassical}}; + const QFTAdderClassical qftAdderClassical{{.addend = "110"}}; const QFTAdderQuantum qftAdderQuantum{{.qubits = 3}}; const QPE qpe{ {.precision = 5, .phase = Phase(1, 3), .method = QPEMethod::Iterative}}; @@ -163,6 +176,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { const auto groverManifest = toManifestJSON(grover); const auto multiplexerManifest = toManifestJSON(multiplexer); const auto qftManifest = toManifestJSON(qft); + const auto qftAdderClassicalManifest = toManifestJSON(qftAdderClassical); const auto qftAdderQuantumManifest = toManifestJSON(qftAdderQuantum); const auto qpeManifest = toManifestJSON(qpe); const auto teleportationManifest = toManifestJSON(teleportation); @@ -173,6 +187,9 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { EXPECT_EQ(toManifestJSON(multiplexerFromManifestJSON(multiplexerManifest)), multiplexerManifest); EXPECT_EQ(toManifestJSON(qftFromManifestJSON(qftManifest)), qftManifest); + EXPECT_EQ(toManifestJSON( + qftAdderClassicalFromManifestJSON(qftAdderClassicalManifest)), + qftAdderClassicalManifest); EXPECT_EQ( toManifestJSON(qftAdderQuantumFromManifestJSON(qftAdderQuantumManifest)), qftAdderQuantumManifest); @@ -185,6 +202,8 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { EXPECT_EQ(benchmarkIdFromManifestJSON(groverManifest), "grover"); EXPECT_EQ(benchmarkIdFromManifestJSON(multiplexerManifest), "multiplexer"); EXPECT_EQ(benchmarkIdFromManifestJSON(qftManifest), "qft"); + EXPECT_EQ(benchmarkIdFromManifestJSON(qftAdderClassicalManifest), + "qft-adder-classical"); EXPECT_EQ(benchmarkIdFromManifestJSON(qftAdderQuantumManifest), "qft-adder-quantum"); EXPECT_EQ(benchmarkIdFromManifestJSON(qpeManifest), "qpe"); @@ -196,6 +215,11 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { std::string::npos); EXPECT_NE(multiplexerManifest.find("\"model\":\"multiplexer\""), std::string::npos); + EXPECT_NE(qftAdderClassicalManifest.find("\"model\":\"qft_adder_classical\""), + std::string::npos); + EXPECT_NE(qftAdderClassicalManifest.find("\"success_outcome\":\"0111\""), + std::string::npos); + EXPECT_NE(qftAdderClassicalManifest.find("\"width\":4"), std::string::npos); EXPECT_NE(qftAdderQuantumManifest.find("\"model\":\"qft_adder_quantum\""), std::string::npos); EXPECT_NE(qftAdderQuantumManifest.find("\"width\":6"), std::string::npos); @@ -217,6 +241,10 @@ TEST(BenchmarkJSON, UsesStableSemanticCaseIds) { caseId(QFT{{.qubits = 3, .periodExponent = 1, .method = QFTMethod::Semiclassical}})); + EXPECT_EQ(caseId(QFTAdderClassical{{.addend = "001"}}), + caseId(QFTAdderClassical{{.addend = "001"}})); + EXPECT_NE(caseId(QFTAdderClassical{{.addend = "001"}}), + caseId(QFTAdderClassical{{.addend = "1"}})); EXPECT_EQ(caseId(QFTAdderQuantum{{.qubits = 3}}), caseId(QFTAdderQuantum{{.qubits = 3}})); EXPECT_NE(caseId(QFTAdderQuantum{{.qubits = 3}}), @@ -300,6 +328,24 @@ TEST(BenchmarkJSON, R"({"schema_version":1,"benchmark":"multiplexer","parameters":{"qubits":7,"angles":[]}})")); }, "unknown key 'angles'"); + expectInvalid( + [] { + static_cast(qftAdderClassicalFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"qft-adder-classical","parameters":{"addend":""}})")); + }, + "between 1 and 1023 bits"); + expectInvalid( + [] { + static_cast(qftAdderClassicalFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"qft-adder-classical","parameters":{"addend":"01x"}})")); + }, + "only '0' and '1'"); + expectInvalid( + [] { + static_cast(qftAdderClassicalFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"qft-adder-classical","parameters":{"addend":"1","qubits":2}})")); + }, + "unknown key 'qubits'"); expectInvalid( [] { static_cast(qftAdderQuantumFromInstanceSpecificationJSON( @@ -378,12 +424,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":"qft-adder-quantum"},{"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-classical"},{"definition_version":1,"id":"qft-adder-quantum"},{"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 qftAdderClassical = describeBenchmarkJSON("qft-adder-classical"); const auto qftAdderQuantum = describeBenchmarkJSON("qft-adder-quantum"); const auto qpe = describeBenchmarkJSON("qpe"); const auto teleportation = describeBenchmarkJSON("teleportation"); @@ -397,6 +444,9 @@ 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(qftAdderClassical.find("\"maxLength\":1023"), std::string::npos); + EXPECT_NE(qftAdderClassical.find("\"pattern\":\"^[01]+$\""), + std::string::npos); EXPECT_NE(qftAdderQuantum.find("\"maximum\":1024"), std::string::npos); EXPECT_NE(qftAdderQuantum.find("\"minimum\":1"), std::string::npos); EXPECT_NE(qpe.find("\"iterative\""), std::string::npos); @@ -446,6 +496,15 @@ TEST(BenchmarkJSON, ParsesCountsAndSerializesEvaluations) { EXPECT_NE(qftAdderQuantumEvaluation.find("\"total_variation_distance\":0.0"), std::string::npos); + const QFTAdderClassical qftAdderClassical{{.addend = "110"}}; + const auto qftAdderClassicalEvaluation = + evaluateJSON(toManifestJSON(qftAdderClassical), + R"({"schema_version":1,"counts":{"0111":8,"0110":2}})"); + EXPECT_NE(qftAdderClassicalEvaluation.find("\"success_probability\":0.8"), + std::string::npos); + EXPECT_NE(qftAdderClassicalEvaluation.find("\"total_variation_distance\":"), + std::string::npos); + const Teleportation teleportation; const auto teleportationEvaluation = evaluateJSON(toManifestJSON(teleportation), diff --git a/test/bench/test_qft_adder_classical.cpp b/test/bench/test_qft_adder_classical.cpp new file mode 100644 index 0000000000..0a00f597d4 --- /dev/null +++ b/test/bench/test_qft_adder_classical.cpp @@ -0,0 +1,77 @@ +/* + * 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/QFTAdderClassical.hpp" + +#include + +#include +#include + +namespace { + +using mqt::bench::Output; +using mqt::bench::QFTAdderClassical; +using mqt::bench::QFTAdderClassicalOptions; + +TEST(QFTAdderClassical, StoresTheAddendAndResult) { + const QFTAdderClassical benchmark{{.addend = "00101"}}; + EXPECT_EQ(benchmark.options().addend, "00101"); + EXPECT_EQ(benchmark.output(), (Output{"result", 6})); + EXPECT_EQ(benchmark.expectedResult(), "000110"); +} + +TEST(QFTAdderClassical, ValidatesTheConfiguredInstance) { + const auto maximum = + std::string(QFTAdderClassicalOptions::MAX_ADDEND_BITS, '1'); + const auto tooLong = + std::string(QFTAdderClassicalOptions::MAX_ADDEND_BITS + 1U, '0'); + const QFTAdderClassical maximumBenchmark{{.addend = maximum}}; + EXPECT_THROW(static_cast(QFTAdderClassical{{.addend = ""}}), + std::invalid_argument); + EXPECT_THROW(static_cast(QFTAdderClassical{{.addend = "10x"}}), + std::invalid_argument); + EXPECT_EQ(maximumBenchmark.expectedResult(), + "1" + std::string(QFTAdderClassicalOptions::MAX_ADDEND_BITS, '0')); + EXPECT_THROW(static_cast(QFTAdderClassical{{.addend = tooLong}}), + std::invalid_argument); +} + +TEST(QFTAdderClassical, AddsOneWithoutTruncatingOverflow) { + const QFTAdderClassical zero{{.addend = "0"}}; + const QFTAdderClassical one{{.addend = "1"}}; + const QFTAdderClassical leadingZeros{{.addend = "001"}}; + const QFTAdderClassical five{{.addend = "101"}}; + const QFTAdderClassical six{{.addend = "110"}}; + const QFTAdderClassical seven{{.addend = "111"}}; + + EXPECT_DOUBLE_EQ(zero.probability("01"), 1.); + EXPECT_DOUBLE_EQ(one.probability("10"), 1.); + EXPECT_DOUBLE_EQ(leadingZeros.probability("0010"), 1.); + EXPECT_DOUBLE_EQ(five.probability("0110"), 1.); + EXPECT_DOUBLE_EQ(six.probability("0111"), 1.); + EXPECT_DOUBLE_EQ(seven.probability("1000"), 1.); + EXPECT_DOUBLE_EQ(seven.probability("0111"), 0.); + EXPECT_THROW(static_cast(seven.probability("000")), + std::invalid_argument); + EXPECT_THROW(static_cast(seven.probability("000x")), + std::invalid_argument); +} + +TEST(QFTAdderClassical, EvaluatesTheDeterministicResult) { + const QFTAdderClassical benchmark{{.addend = "101"}}; + const auto evaluation = benchmark.evaluate({{"0110", 80}, {"0101", 20}}); + EXPECT_DOUBLE_EQ(evaluation.totalVariationDistance, 0.2); + EXPECT_DOUBLE_EQ(evaluation.squaredHellingerFidelity, 0.8); + ASSERT_TRUE(evaluation.successProbability); + EXPECT_DOUBLE_EQ(*evaluation.successProbability, 0.8); +} + +} // namespace diff --git a/test/python/test_bench.py b/test/python/test_bench.py index 73f36a9085..87a8aad1a4 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -16,7 +16,17 @@ import pytest from mqt.core import bench, mlir -from mqt.core.bench import bv, ghz, grover, multiplexer, qft, qft_adder_quantum, qpe, teleportation +from mqt.core.bench import ( + bv, + ghz, + grover, + multiplexer, + qft, + qft_adder_classical, + qft_adder_quantum, + qpe, + teleportation, +) def assert_generates( @@ -26,6 +36,7 @@ def assert_generates( | grover.Grover | multiplexer.Multiplexer | qft.QFT + | qft_adder_classical.QFTAdderClassical | qft_adder_quantum.QFTAdderQuantum | qpe.QPE | teleportation.Teleportation @@ -171,6 +182,34 @@ def test_quantum_qft_adder_reference_json_and_generation() -> None: assert_generates(benchmark) +def test_classical_qft_adder_reference_json_and_generation() -> None: + """Expose exact classical addition without truncating a carry.""" + benchmark = qft_adder_classical.QFTAdderClassical(qft_adder_classical.Options(addend="110")) + assert benchmark.options.addend == "110" + assert benchmark.output.name == "result" + assert benchmark.output.width == 4 + assert benchmark.expected_result == "0111" + assert benchmark.probability("0111") == 1 + assert benchmark.probability("0110") == 0 + + evaluation = benchmark.evaluate({"0111": 8, "0110": 2}) + assert evaluation.total_variation_distance == pytest.approx(0.2) + assert evaluation.squared_hellinger_fidelity == pytest.approx(0.8) + assert evaluation.success_probability == pytest.approx(0.8) + assert json.loads(benchmark.instance_specification_json)["parameters"] == {"addend": "110"} + + instance_copy = qft_adder_classical.QFTAdderClassical.from_instance_specification_json( + benchmark.instance_specification_json + ) + manifest_copy = qft_adder_classical.QFTAdderClassical.from_manifest_json(benchmark.manifest_json) + assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id + + shots = 1_024 + counts = benchmark.generate().to_qco().sample(shots=shots, seed=17) + assert counts == {"0111": shots} + assert_generates(benchmark) + + def test_qpe_accepts_fraction_and_native_phase() -> None: """Use exact rational input without a free-form parameter dictionary.""" options = qpe.Options( diff --git a/test/python/test_cli.py b/test/python/test_cli.py index 2708494f17..fc895d1863 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-classical"' in ret.stdout assert '"qft-adder-quantum"' in ret.stdout assert '"qpe"' in ret.stdout assert '"teleportation"' in ret.stdout From 57147cf9c89239cd59528eb099c47b0825ce09b3 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:01:44 +0200 Subject: [PATCH 12/24] Update changelog Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/qft-adder-classical.md | 5 ++--- CHANGELOG.md | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.agent/plans/qft-adder-classical.md b/.agent/plans/qft-adder-classical.md index d27076c7f3..c04bdb2214 100644 --- a/.agent/plans/qft-adder-classical.md +++ b/.agent/plans/qft-adder-classical.md @@ -1,7 +1,6 @@ # Add a classical-input QFT adder benchmark -Status: implementation complete. The stacked draft pull request and changelog -reference remain to be completed. +Status: complete. ## Goal and scope @@ -44,5 +43,5 @@ combines each wire's classically known rotations into one single-qubit phase. - [x] Generate and structurally test the complete Figure 3 circuit. - [x] Document the source, harness, bit order, phase convention, and output. - [x] Validate the focused native, MLIR, and Python behavior. -- [ ] Create the draft pull request on the quantum-input QFT-adder branch and +- [x] Create the draft pull request on the quantum-input QFT-adder branch and add its number to the rolling structured-benchmark changelog entry. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b9aca651f..ecee6ceb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,8 @@ 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], [#2404]) ([**@burgholzer**], [**@denialhaag**]) + [#2337], [#2380], [#2402], [#2404], [#2408]) ([**@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 +928,7 @@ for previous changelogs._ [#2421]: https://github.com/munich-quantum-toolkit/core/pull/2421 +[#2408]: https://github.com/munich-quantum-toolkit/core/pull/2408 [#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 From 9937262ab5f5bbc454f7ebb689f6d5e6ab97dd50 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:33:42 +0200 Subject: [PATCH 13/24] Fix linter errors Assisted-by: GPT-5.6 Sol via Codex --- src/bench/QFTAdderClassical.cpp | 5 ++++- test/bench/test_qft_adder_classical.cpp | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bench/QFTAdderClassical.cpp b/src/bench/QFTAdderClassical.cpp index 413add5856..c636f048dd 100644 --- a/src/bench/QFTAdderClassical.cpp +++ b/src/bench/QFTAdderClassical.cpp @@ -21,8 +21,9 @@ #include namespace mqt::bench { +namespace { -[[nodiscard]] static std::string increment(const std::string_view addend) { +[[nodiscard]] std::string increment(const std::string_view addend) { auto result = std::string{"0"} + std::string{addend}; auto carry = true; for (size_t index = result.size(); index > 0 && carry; --index) { @@ -33,6 +34,8 @@ namespace mqt::bench { return result; } +} // namespace + QFTAdderClassical::QFTAdderClassical(QFTAdderClassicalOptions options) : options_(std::move(options)), output_{.name = "result", .width = options_.addend.size() + 1U} { diff --git a/test/bench/test_qft_adder_classical.cpp b/test/bench/test_qft_adder_classical.cpp index 0a00f597d4..c3ff455cdd 100644 --- a/test/bench/test_qft_adder_classical.cpp +++ b/test/bench/test_qft_adder_classical.cpp @@ -8,6 +8,7 @@ * Licensed under the MIT License */ +#include "bench/Evaluation.hpp" #include "bench/QFTAdderClassical.hpp" #include From 0113a1c4b537e0a4d8c8826d600a82ae099ca381 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:27:26 +0200 Subject: [PATCH 14/24] Refine classical-input QFT adder Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/qft-adder-classical.md | 14 +- bindings/bench/register_bench.cpp | 3 +- .../bench/register_qft_adder_classical.cpp | 3 +- docs/benchmarks.md | 12 - mlir/bench/programs/QFTAdderClassical.cpp | 31 ++- mlir/unittests/bench/QFTAdderTestUtils.h | 208 ------------------ ...benchmark_generate_qft_adder_classical.cpp | 200 +++++------------ python/mqt/core/bench/qft_adder_classical.pyi | 7 +- 8 files changed, 98 insertions(+), 380 deletions(-) delete mode 100644 mlir/unittests/bench/QFTAdderTestUtils.h diff --git a/.agent/plans/qft-adder-classical.md b/.agent/plans/qft-adder-classical.md index c04bdb2214..0b7798347a 100644 --- a/.agent/plans/qft-adder-classical.md +++ b/.agent/plans/qft-adder-classical.md @@ -18,11 +18,12 @@ one; the extra accumulator qubit preserves overflow. ## Decisions The generator uses the shared exact no-swap QFT and inverse-QFT helpers in -`mlir/bench/programs/QFTAdderUtils.*`. Between them, it emits exactly one -unconditional phase gate for each accumulator wire, including a zero-angle gate. -For little-endian accumulator wire `j`, the phase is the binary fraction formed -by addend bits `j` through zero. The extra wire receives the continued fraction -and therefore records carry. +`mlir/bench/programs/QFTUtils.*`. Between them, it materializes the precomputed +angles as a dense tensor. A structured loop applies one unconditional phase gate +to each accumulator wire, including a zero-angle gate. For little-endian +accumulator wire `j`, the phase is the binary fraction formed by addend bits `j` +through zero. The extra wire receives the continued fraction and therefore +records carry. Compute the phase table by scanning the addend from least to most significant: divide the previous angle by two and add pi for a set bit. Append one more @@ -40,7 +41,8 @@ combines each wire's classically known rotations into one single-qubit phase. - [x] Add the typed family, strict JSON contract, binding, stubs, and reference tests. -- [x] Generate and structurally test the complete Figure 3 circuit. +- [x] Generate the complete Figure 3 circuit and test its phase table and + structured size. - [x] Document the source, harness, bit order, phase convention, and output. - [x] Validate the focused native, MLIR, and Python behavior. - [x] Create the draft pull request on the quantum-input QFT-adder branch and diff --git a/bindings/bench/register_bench.cpp b/bindings/bench/register_bench.cpp index 03aa034e7b..37d85f03ff 100644 --- a/bindings/bench/register_bench.cpp +++ b/bindings/bench/register_bench.cpp @@ -73,7 +73,8 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { const nb::module_ qftAdderClassical = m.def_submodule("qft_adder_classical", - "Classical-input QFT adder instances and options."); + "Classical-input QFT adder benchmark instances and " + "options."); registerQFTAdderClassical(qftAdderClassical); const nb::module_ qftAdderQuantum = m.def_submodule( diff --git a/bindings/bench/register_qft_adder_classical.cpp b/bindings/bench/register_qft_adder_classical.cpp index 0aec277cf4..0d6e75b28d 100644 --- a/bindings/bench/register_qft_adder_classical.cpp +++ b/bindings/bench/register_qft_adder_classical.cpp @@ -33,7 +33,8 @@ void registerQFTAdderClassical(const nb::module_& m) { auto qftAdder = nb::class_( m, "QFTAdderClassical", - "A validated classical-input QFT adder benchmark."); + "A validated classical-input QFT adder benchmark.\n\n" + "Reference: https://arxiv.org/abs/quant-ph/0205095"); qftAdder.def(nb::init(), "options"_a) .def_prop_ro("options", &bench::QFTAdderClassical::options, nb::rv_policy::reference_internal, diff --git a/docs/benchmarks.md b/docs/benchmarks.md index c48bc5c2b6..9634dfffbe 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -53,18 +53,6 @@ print("Width:", benchmark.output.width) Each family validates its instance when it creates one. Fixed families need no options. -## Classical-input QFT adder - -The `qft-adder-classical` family implements the classical-input QFT adder from -[Beauregard's circuit for Shor's algorithm](https://arxiv.org/abs/quant-ph/0205095). -The `addend` parameter is a big-endian binary string. Leading zeros define the -input width. The benchmark prepares an accumulator in state |1>, applies the -exact no-swap QFT, one combined phase gate for each Fourier qubit, and the -inverse QFT. - -For an `n`-bit addend, the result has `n + 1` bits. The extra qubit retains the -carry, so the deterministic result is the zero-extended addend plus one. - ## Inspect the canonical instance specification and manifest A canonical instance specification records every resolved default. A manifest diff --git a/mlir/bench/programs/QFTAdderClassical.cpp b/mlir/bench/programs/QFTAdderClassical.cpp index cc46747821..5a1f01f4a8 100644 --- a/mlir/bench/programs/QFTAdderClassical.cpp +++ b/mlir/bench/programs/QFTAdderClassical.cpp @@ -11,13 +11,18 @@ #include "bench/QFTAdderClassical.hpp" #include "Programs.h" -#include "QFTAdderUtils.h" +#include "QFTUtils.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" +#include +#include +#include +#include +#include #include +#include #include -#include #include #include #include @@ -27,8 +32,8 @@ namespace mqt::bench { using namespace mlir; -[[nodiscard]] static SmallVector -phaseAngles(const std::string_view addend) { +[[nodiscard]] static Value phaseAngles(qc::QCProgramBuilder& builder, + const std::string_view addend) { SmallVector angles; angles.reserve(addend.size() + 1U); long double angle = 0.L; @@ -40,7 +45,11 @@ phaseAngles(const std::string_view addend) { angles.push_back(static_cast(angle)); } angles.push_back(static_cast(angle / 2.L)); - return angles; + + 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 qftAdderClassical(qc::QCProgramBuilder& builder, @@ -55,12 +64,12 @@ SmallVector qftAdderClassical(qc::QCProgramBuilder& builder, builder.x(builder.loadQubit(sum, zero)); detail::forwardQFT(builder, sum, qubits); - const auto angles = phaseAngles(benchmark.options().addend); - for (size_t target = 0; target < angles.size(); ++target) { - auto angle = builder.floatConstant(angles[target]); - auto index = builder.indexConstant(static_cast(target)); - builder.p(angle, builder.loadQubit(sum, index)); - } + auto angles = phaseAngles(builder, benchmark.options().addend); + builder.scfFor(0, qubits, 1, [&](Value target) { + auto angle = tensor::ExtractOp::create(builder, angles, ValueRange{target}) + .getResult(); + builder.p(angle, builder.loadQubit(sum, target)); + }); detail::inverseQFT(builder, sum, qubits); builder.measureQubitRegister(sum, result, qubits); diff --git a/mlir/unittests/bench/QFTAdderTestUtils.h b/mlir/unittests/bench/QFTAdderTestUtils.h deleted file mode 100644 index a9f2385901..0000000000 --- a/mlir/unittests/bench/QFTAdderTestUtils.h +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 "mlir/Dialect/QC/IR/QCOps.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace mqt::bench::test { - -using namespace mlir; - -inline void expectConstantIndex(Value value, const int64_t expected) { - auto constant = value.getDefiningOp(); - ASSERT_TRUE(constant); - EXPECT_EQ(constant.value(), expected); -} - -inline void expectConstantFloat(Value value, const double expected) { - auto constant = value.getDefiningOp(); - ASSERT_TRUE(constant); - auto attribute = dyn_cast(constant.getValue()); - ASSERT_TRUE(attribute); - EXPECT_DOUBLE_EQ(attribute.getValueAsDouble(), expected); -} - -inline void expectStaticLoop(scf::ForOp loop, const int64_t lower, - const int64_t upper) { - expectConstantIndex(loop.getLowerBound(), lower); - expectConstantIndex(loop.getUpperBound(), upper); - expectConstantIndex(loop.getStep(), 1); -} - -[[nodiscard]] inline SmallVector topLevelLoops(ModuleOp moduleOp) { - SmallVector loops; - moduleOp.walk([&](scf::ForOp loop) { - if (!loop->getParentOfType()) { - loops.push_back(loop); - } - }); - return loops; -} - -[[nodiscard]] inline SmallVector nestedLoops(scf::ForOp outer) { - SmallVector loops; - outer.walk([&](scf::ForOp loop) { - if (loop != outer) { - loops.push_back(loop); - } - }); - return loops; -} - -inline void expectAngleRecurrence(scf::ForOp loop, Value initialAngle, - const double factor) { - ASSERT_EQ(loop.getInitArgs().size(), 1U); - EXPECT_EQ(loop.getInitArgs().front(), initialAngle); - auto angle = loop.getRegionIterArg(0); - - auto yield = dyn_cast(loop.getBody()->getTerminator()); - ASSERT_TRUE(yield); - ASSERT_EQ(yield.getNumOperands(), 1U); - auto scale = yield.getOperand(0).getDefiningOp(); - ASSERT_TRUE(scale); - EXPECT_EQ(scale.getLhs(), angle); - expectConstantFloat(scale.getRhs(), factor); -} - -struct ControlledPhase { - qc::CtrlOp control; - qc::POp phase; -}; - -[[nodiscard]] inline ControlledPhase controlledPhase(scf::ForOp loop) { - ControlledPhase result; - size_t controls = 0; - size_t phases = 0; - loop.walk([&](qc::CtrlOp op) { - result.control = op; - ++controls; - }); - loop.walk([&](qc::POp op) { - result.phase = op; - ++phases; - }); - EXPECT_EQ(controls, 1U); - EXPECT_EQ(phases, 1U); - if (result.control) { - EXPECT_EQ(result.control.getNumControls(), 1U); - EXPECT_EQ(result.control.getNumTargets(), 1U); - } - return result; -} - -inline void expectForwardQFT(scf::ForOp forward, Value qubitRegister, - const int64_t qubits) { - qc::HOp forwardH; - forward.walk([&](qc::HOp op) { forwardH = op; }); - ASSERT_TRUE(forwardH); - auto targetLoad = forwardH.getQubit(0).getDefiningOp(); - ASSERT_TRUE(targetLoad); - EXPECT_EQ(targetLoad.getMemref(), qubitRegister); - auto target = targetLoad.getIndices().front(); - auto targetExpression = target.getDefiningOp(); - ASSERT_TRUE(targetExpression); - expectConstantIndex(targetExpression.getLhs(), qubits - 1); - EXPECT_EQ(targetExpression.getRhs(), forward.getInductionVar()); - - auto innerLoops = nestedLoops(forward); - ASSERT_EQ(innerLoops.size(), 1U); - auto inner = innerLoops.front(); - EXPECT_TRUE(forwardH->isBeforeInBlock(inner)); - expectConstantIndex(inner.getLowerBound(), 0); - EXPECT_EQ(inner.getUpperBound(), target); - expectConstantIndex(inner.getStep(), 1); - ASSERT_EQ(inner.getInitArgs().size(), 1U); - expectConstantFloat(inner.getInitArgs().front(), std::numbers::pi / 2.); - expectAngleRecurrence(inner, inner.getInitArgs().front(), 0.5); - - auto controlled = controlledPhase(inner); - ASSERT_TRUE(controlled.control); - ASSERT_TRUE(controlled.phase); - EXPECT_EQ(controlled.phase.getTheta(), inner.getRegionIterArg(0)); - auto controlLoad = - controlled.control.getControl(0).getDefiningOp(); - auto phaseTargetLoad = - controlled.control.getTarget(0).getDefiningOp(); - ASSERT_TRUE(controlLoad); - ASSERT_TRUE(phaseTargetLoad); - EXPECT_EQ(controlLoad.getMemref(), qubitRegister); - EXPECT_EQ(phaseTargetLoad.getMemref(), qubitRegister); - EXPECT_EQ(phaseTargetLoad.getIndices().front(), target); - auto controlExpression = - controlLoad.getIndices().front().getDefiningOp(); - ASSERT_TRUE(controlExpression); - EXPECT_EQ(controlExpression.getRhs(), inner.getInductionVar()); - auto previous = controlExpression.getLhs().getDefiningOp(); - ASSERT_TRUE(previous); - expectConstantIndex(previous.getLhs(), qubits - 2); - EXPECT_EQ(previous.getRhs(), forward.getInductionVar()); -} - -inline void expectInverseQFT(scf::ForOp inverse, Value qubitRegister) { - ASSERT_EQ(inverse.getInitArgs().size(), 1U); - expectConstantFloat(inverse.getInitArgs().front(), -std::numbers::pi); - auto innerLoops = nestedLoops(inverse); - ASSERT_EQ(innerLoops.size(), 1U); - auto inner = innerLoops.front(); - expectConstantIndex(inner.getLowerBound(), 0); - EXPECT_EQ(inner.getUpperBound(), inverse.getInductionVar()); - expectConstantIndex(inner.getStep(), 1); - expectAngleRecurrence(inner, inverse.getRegionIterArg(0), 2.); - - auto controlled = controlledPhase(inner); - ASSERT_TRUE(controlled.control); - ASSERT_TRUE(controlled.phase); - EXPECT_EQ(controlled.phase.getTheta(), inner.getRegionIterArg(0)); - auto controlLoad = - controlled.control.getControl(0).getDefiningOp(); - auto targetLoad = - controlled.control.getTarget(0).getDefiningOp(); - ASSERT_TRUE(controlLoad); - ASSERT_TRUE(targetLoad); - EXPECT_EQ(controlLoad.getMemref(), qubitRegister); - EXPECT_EQ(controlLoad.getIndices().front(), inner.getInductionVar()); - EXPECT_EQ(targetLoad.getMemref(), qubitRegister); - EXPECT_EQ(targetLoad.getIndices().front(), inverse.getInductionVar()); - - qc::HOp inverseH; - inverse.walk([&](qc::HOp op) { inverseH = op; }); - ASSERT_TRUE(inverseH); - EXPECT_TRUE(inner->isBeforeInBlock(inverseH)); - auto hLoad = inverseH.getQubit(0).getDefiningOp(); - ASSERT_TRUE(hLoad); - EXPECT_EQ(hLoad.getMemref(), qubitRegister); - EXPECT_EQ(hLoad.getIndices().front(), inverse.getInductionVar()); - - auto yield = dyn_cast(inverse.getBody()->getTerminator()); - ASSERT_TRUE(yield); - ASSERT_EQ(yield.getNumOperands(), 1U); - auto nextAngle = yield.getOperand(0).getDefiningOp(); - ASSERT_TRUE(nextAngle); - EXPECT_EQ(nextAngle.getLhs(), inverse.getRegionIterArg(0)); - expectConstantFloat(nextAngle.getRhs(), 0.5); -} - -} // namespace mqt::bench::test diff --git a/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp index 53a51596bd..c34b548f30 100644 --- a/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp @@ -8,185 +8,107 @@ * Licensed under the MIT License */ -#include "QFTAdderTestUtils.h" #include "TestUtils.h" #include "bench/QFTAdderClassical.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; -namespace { - -struct ClassicalAdderCase { - std::string addend; - std::vector angles; -}; - -class ClassicalQFTAdderStructureTest - : public testing::TestWithParam {}; +static void expectPhaseLoopConstantIndex(Value value, int64_t expected) { + auto constant = value.getDefiningOp(); + ASSERT_TRUE(constant); + EXPECT_EQ(constant.value(), expected); +} -} // namespace +[[nodiscard]] static DenseElementsAttr phaseTable(ModuleOp moduleOp) { + DenseElementsAttr result; + moduleOp.walk([&](arith::ConstantOp op) { + if (auto table = dyn_cast(op.getValue())) { + EXPECT_FALSE(result); + result = table; + } + }); + return result; +} -TEST_P(ClassicalQFTAdderStructureTest, EmitsExactClassicalQFTAdderSchedule) { - const auto& testCase = GetParam(); - const auto qubits = static_cast(testCase.addend.size() + 1U); - auto program = generate(QFTAdderClassical{{.addend = testCase.addend}}); +TEST(GenerateProgramTest, UsesConfiguredClassicalQFTAdderPhases) { + auto program = generate(QFTAdderClassical{{.addend = "101"}}); ASSERT_TRUE(program); auto moduleOp = program->module(); - EXPECT_EQ(test::countOps(moduleOp), 1U); - EXPECT_EQ(test::countOps(moduleOp), 1U); - EXPECT_EQ(test::countOps(moduleOp), 2U); - EXPECT_EQ(test::countOps(moduleOp), 1U); - EXPECT_EQ(test::countOps(moduleOp), 2U); - EXPECT_EQ(test::countOps(moduleOp), testCase.angles.size() + 2U); - EXPECT_EQ(test::countOps(moduleOp), 1U); - EXPECT_EQ(test::countOps(moduleOp), 0U); - EXPECT_EQ(test::countOps(moduleOp), 0U); - EXPECT_EQ(test::countOps(moduleOp), 5U); - - cbit::AllocOp resultAllocation; - moduleOp.walk([&](cbit::AllocOp op) { resultAllocation = op; }); - ASSERT_TRUE(resultAllocation); - EXPECT_EQ(resultAllocation.getResult().getType().getWidth(), qubits); - - auto loops = test::topLevelLoops(moduleOp); - ASSERT_EQ(loops.size(), 3U); - for (auto loop : loops) { - test::expectStaticLoop(loop, 0, qubits); - } - - qc::XOp prepareOne; - moduleOp.walk([&](qc::XOp op) { prepareOne = op; }); - ASSERT_TRUE(prepareOne); - auto oneLoad = prepareOne.getQubit(0).getDefiningOp(); - ASSERT_TRUE(oneLoad); - test::expectConstantIndex(oneLoad.getIndices().front(), 0); - auto sum = oneLoad.getMemref(); - - auto forward = loops[0]; - EXPECT_TRUE(prepareOne->isBeforeInBlock(forward)); - test::expectForwardQFT(forward, sum, qubits); - - // Beauregard's classical-input optimization combines all known rotations - // into one unconditional phase per wire, including the overflow wire. - SmallVector additionPhases; + auto table = phaseTable(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()) { - additionPhases.push_back(op); + EXPECT_FALSE(phase); + phase = op; } }); - ASSERT_EQ(additionPhases.size(), testCase.angles.size()); - for (size_t target = 0; target < additionPhases.size(); ++target) { - auto phase = additionPhases[target]; - EXPECT_FALSE(phase->getParentOfType()); - test::expectConstantFloat(phase.getTheta(), testCase.angles[target]); - auto targetLoad = phase.getQubit(0).getDefiningOp(); - ASSERT_TRUE(targetLoad); - EXPECT_EQ(targetLoad.getMemref(), sum); - test::expectConstantIndex(targetLoad.getIndices().front(), - static_cast(target)); - EXPECT_TRUE(forward->isBeforeInBlock(phase)); - if (target != 0U) { - EXPECT_TRUE(additionPhases[target - 1]->isBeforeInBlock(phase)); - } - } - - auto inverse = loops[1]; - EXPECT_TRUE(additionPhases.back()->isBeforeInBlock(inverse)); - test::expectInverseQFT(inverse, sum); - - // Register bit zero is least significant. Equal source and destination - // indices therefore produce the declared big-endian result register. - auto measurementLoop = loops[2]; - EXPECT_TRUE(inverse->isBeforeInBlock(measurementLoop)); - qc::MeasureOp measurement; - measurementLoop.walk([&](qc::MeasureOp op) { measurement = op; }); - ASSERT_TRUE(measurement); - auto measured = measurement.getQubit().getDefiningOp(); - ASSERT_TRUE(measured); - EXPECT_EQ(measured.getMemref(), sum); - EXPECT_EQ(measured.getIndices().front(), measurementLoop.getInductionVar()); - auto store = dyn_cast(*measurement.getResult().user_begin()); - ASSERT_TRUE(store); - EXPECT_EQ(store.getReg(), resultAllocation.getResult()); - EXPECT_EQ(store.getIndex(), measurementLoop.getInductionVar()); + 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, KeepsLargestClassicalQFTAdderFiniteAndSerializable) { +TEST(GenerateProgramTest, KeepsLargestClassicalQFTAdderFiniteAndStructured) { auto addend = std::string(QFTAdderClassicalOptions::MAX_ADDEND_BITS, '1'); auto program = generate(QFTAdderClassical{{.addend = std::move(addend)}}); ASSERT_TRUE(program); auto moduleOp = program->module(); - EXPECT_EQ(test::countOps(moduleOp), - QFTAdderClassicalOptions::MAX_ADDEND_BITS + 3U); - const auto operations = test::countOperations(moduleOp); - EXPECT_GT(operations, QFTAdderClassicalOptions::MAX_ADDEND_BITS); - EXPECT_LT(operations, 5U * QFTAdderClassicalOptions::MAX_ADDEND_BITS); - moduleOp.walk([&](arith::ConstantOp op) { - if (const auto value = dyn_cast(op.getValue())) { - EXPECT_TRUE(std::isfinite(value.getValueAsDouble())); - } - }); - test::expectJeffRoundTrip(std::move(*program)); -} + auto table = phaseTable(moduleOp); + ASSERT_TRUE(table); + EXPECT_EQ(table.getNumElements(), + QFTAdderClassicalOptions::MAX_ADDEND_BITS + 1U); + for (const auto angle : table.getValues()) { + EXPECT_TRUE(std::isfinite(angle)); + } -INSTANTIATE_TEST_SUITE_P( - ExactPhases, ClassicalQFTAdderStructureTest, - testing::Values(ClassicalAdderCase{"0", {0., 0.}}, - ClassicalAdderCase{ - "1", {std::numbers::pi, std::numbers::pi / 2.}}, - ClassicalAdderCase{"101", - { - std::numbers::pi, - std::numbers::pi / 2., - 5. * std::numbers::pi / 4., - 5. * std::numbers::pi / 8., - }}, - ClassicalAdderCase{"111", - { - std::numbers::pi, - 3. * std::numbers::pi / 2., - 7. * std::numbers::pi / 4., - 7. * std::numbers::pi / 8., - }}, - ClassicalAdderCase{"110", - { - 0., - std::numbers::pi, - 3. * std::numbers::pi / 2., - 3. * std::numbers::pi / 4., - }}, - ClassicalAdderCase{"001", - { - std::numbers::pi, - std::numbers::pi / 2., - std::numbers::pi / 4., - std::numbers::pi / 8., - }})); + EXPECT_EQ(test::countOps(moduleOp), 1U); + EXPECT_LT(test::countOperations(moduleOp), 100U); +} } // namespace mqt::bench diff --git a/python/mqt/core/bench/qft_adder_classical.pyi b/python/mqt/core/bench/qft_adder_classical.pyi index 0e07510f04..6d06327521 100644 --- a/python/mqt/core/bench/qft_adder_classical.pyi +++ b/python/mqt/core/bench/qft_adder_classical.pyi @@ -6,7 +6,7 @@ # # Licensed under the MIT License -"""Classical-input QFT adder instances and options.""" +"""Classical-input QFT adder benchmark instances and options.""" from collections.abc import Mapping @@ -22,7 +22,10 @@ class Options: """The big-endian classical addend.""" class QFTAdderClassical: - """A validated classical-input QFT adder benchmark.""" + """A validated classical-input QFT adder benchmark. + + Reference: https://arxiv.org/abs/quant-ph/0205095 + """ def __init__(self, options: Options) -> None: ... @property From 9d9ac312b9f0115776b9c7bf3c5626b099bf774b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 12:16:12 +0000 Subject: [PATCH 15/24] =?UTF-8?q?=F0=9F=A7=AA=20Check=20classical=20adder?= =?UTF-8?q?=20carry=20with=20DD=20sampling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the fixed accumulator and result width. Exercise zero, leading zeros, and overflow with DD execution after folding phase-table reads. Share the phase-table assertion helper with QPE tests. Assisted-by: GPT-6 via Codex --- bindings/bench/register_qft_adder_classical.cpp | 5 ++++- include/mqt-core/bench/QFTAdderClassical.hpp | 4 +++- mlir/unittests/bench/TestUtils.h | 15 +++++++++++++++ ...t_benchmark_generate_qft_adder_classical.cpp | 15 ++------------- .../bench/test_benchmark_generate_qpe.cpp | 17 +++-------------- python/mqt/core/bench/qft_adder_classical.pyi | 4 +++- test/python/test_bench.py | 17 ++++++++++++++--- 7 files changed, 44 insertions(+), 33 deletions(-) diff --git a/bindings/bench/register_qft_adder_classical.cpp b/bindings/bench/register_qft_adder_classical.cpp index 0d6e75b28d..ebaf67116b 100644 --- a/bindings/bench/register_qft_adder_classical.cpp +++ b/bindings/bench/register_qft_adder_classical.cpp @@ -33,7 +33,10 @@ void registerQFTAdderClassical(const nb::module_& m) { auto qftAdder = nb::class_( m, "QFTAdderClassical", - "A validated classical-input QFT adder benchmark.\n\n" + "A QFT adder that adds the classical addend to an accumulator " + "in |1>.\n\n" + "Leading zeros define the n-bit input width. The n+1-bit big-endian " + "result is addend + 1; the extra bit preserves carry.\n\n" "Reference: https://arxiv.org/abs/quant-ph/0205095"); qftAdder.def(nb::init(), "options"_a) .def_prop_ro("options", &bench::QFTAdderClassical::options, diff --git a/include/mqt-core/bench/QFTAdderClassical.hpp b/include/mqt-core/bench/QFTAdderClassical.hpp index ec4a7fa510..c8981c90a8 100644 --- a/include/mqt-core/bench/QFTAdderClassical.hpp +++ b/include/mqt-core/bench/QFTAdderClassical.hpp @@ -27,7 +27,9 @@ struct QFTAdderClassicalOptions { std::string addend; }; -/// A validated classical-input QFT adder and its analytic reference. +/// A QFT adder that adds the classical addend to an accumulator in |1>. +/// Leading zeros define the n-bit input width. The n+1-bit big-endian result +/// is addend + 1; the extra bit preserves carry. class MQT_CORE_BENCH_EXPORT QFTAdderClassical final { public: explicit QFTAdderClassical(QFTAdderClassicalOptions options); 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_generate_qft_adder_classical.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp index c34b548f30..20d1c11b4b 100644 --- a/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp @@ -40,23 +40,12 @@ static void expectPhaseLoopConstantIndex(Value value, int64_t expected) { EXPECT_EQ(constant.value(), expected); } -[[nodiscard]] static DenseElementsAttr phaseTable(ModuleOp moduleOp) { - DenseElementsAttr result; - moduleOp.walk([&](arith::ConstantOp op) { - if (auto table = dyn_cast(op.getValue())) { - EXPECT_FALSE(result); - result = table; - } - }); - return result; -} - TEST(GenerateProgramTest, UsesConfiguredClassicalQFTAdderPhases) { auto program = generate(QFTAdderClassical{{.addend = "101"}}); ASSERT_TRUE(program); auto moduleOp = program->module(); - auto table = phaseTable(moduleOp); + auto table = test::angleTable(moduleOp); ASSERT_TRUE(table); const auto angles = llvm::to_vector(table.getValues()); ASSERT_EQ(angles.size(), 4U); @@ -99,7 +88,7 @@ TEST(GenerateProgramTest, KeepsLargestClassicalQFTAdderFiniteAndStructured) { ASSERT_TRUE(program); auto moduleOp = program->module(); - auto table = phaseTable(moduleOp); + auto table = test::angleTable(moduleOp); ASSERT_TRUE(table); EXPECT_EQ(table.getNumElements(), QFTAdderClassicalOptions::MAX_ADDEND_BITS + 1U); 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/qft_adder_classical.pyi b/python/mqt/core/bench/qft_adder_classical.pyi index 6d06327521..7ec09647bc 100644 --- a/python/mqt/core/bench/qft_adder_classical.pyi +++ b/python/mqt/core/bench/qft_adder_classical.pyi @@ -22,7 +22,9 @@ class Options: """The big-endian classical addend.""" class QFTAdderClassical: - """A validated classical-input QFT adder benchmark. + """A QFT adder that adds the classical addend to an accumulator in |1>. + + Leading zeros define the n-bit input width. The n+1-bit big-endian result is addend + 1; the extra bit preserves carry. Reference: https://arxiv.org/abs/quant-ph/0205095 """ diff --git a/test/python/test_bench.py b/test/python/test_bench.py index 87a8aad1a4..fbbaed67c7 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -204,12 +204,23 @@ def test_classical_qft_adder_reference_json_and_generation() -> None: manifest_copy = qft_adder_classical.QFTAdderClassical.from_manifest_json(benchmark.manifest_json) assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id - shots = 1_024 - counts = benchmark.generate().to_qco().sample(shots=shots, seed=17) - assert counts == {"0111": shots} assert_generates(benchmark) +@pytest.mark.parametrize( + ("addend", "expected"), + [("0", "01"), ("1", "10"), ("001", "0010"), ("110", "0111"), ("111", "1000")], +) +def test_classical_qft_adder_dd_sampling_preserves_width_and_carry(addend: str, expected: str) -> None: + """Execute zero, leading-zero, and carry cases against their exact sums.""" + benchmark = qft_adder_classical.QFTAdderClassical(qft_adder_classical.Options(addend=addend)) + program = benchmark.generate().to_qco() + # Fold phase-table reads to scalars supported by the DD interpreter. + program.unroll_quantum_loops() + shots = 1_024 + assert program.sample(shots=shots, seed=17) == {expected: shots} + + def test_qpe_accepts_fraction_and_native_phase() -> None: """Use exact rational input without a free-form parameter dictionary.""" options = qpe.Options( From 1e9a7352b0b7b67df35fdc9de5274ef587f418c7 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:51:53 +0200 Subject: [PATCH 16/24] Improve classical QFT adder docstring Assisted-by: GPT-5.6 Sol via Codex --- bindings/bench/register_qft_adder_classical.cpp | 11 ++++++----- python/mqt/core/bench/qft_adder_classical.pyi | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bindings/bench/register_qft_adder_classical.cpp b/bindings/bench/register_qft_adder_classical.cpp index ebaf67116b..7670031eb6 100644 --- a/bindings/bench/register_qft_adder_classical.cpp +++ b/bindings/bench/register_qft_adder_classical.cpp @@ -33,11 +33,12 @@ void registerQFTAdderClassical(const nb::module_& m) { auto qftAdder = nb::class_( m, "QFTAdderClassical", - "A QFT adder that adds the classical addend to an accumulator " - "in |1>.\n\n" - "Leading zeros define the n-bit input width. The n+1-bit big-endian " - "result is addend + 1; the extra bit preserves carry.\n\n" - "Reference: https://arxiv.org/abs/quant-ph/0205095"); + R"pb(A QFT adder that adds the classical addend to an accumulator in :math:`|1\\rangle`. + +Leading zeros define the n-bit input width. The n+1-bit big-endian result is +addend + 1; the extra bit preserves carry. + +Reference: https://arxiv.org/abs/quant-ph/0205095)pb"); qftAdder.def(nb::init(), "options"_a) .def_prop_ro("options", &bench::QFTAdderClassical::options, nb::rv_policy::reference_internal, diff --git a/python/mqt/core/bench/qft_adder_classical.pyi b/python/mqt/core/bench/qft_adder_classical.pyi index 7ec09647bc..6b05fd7940 100644 --- a/python/mqt/core/bench/qft_adder_classical.pyi +++ b/python/mqt/core/bench/qft_adder_classical.pyi @@ -22,9 +22,10 @@ class Options: """The big-endian classical addend.""" class QFTAdderClassical: - """A QFT adder that adds the classical addend to an accumulator in |1>. + """A QFT adder that adds the classical addend to an accumulator in :math:`|1\\\\rangle`. - Leading zeros define the n-bit input width. The n+1-bit big-endian result is addend + 1; the extra bit preserves carry. + Leading zeros define the n-bit input width. The n+1-bit big-endian result is + addend + 1; the extra bit preserves carry. Reference: https://arxiv.org/abs/quant-ph/0205095 """ From 6799c87ac4e047a9f1d7939221ccb0824251efc5 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 12:52:00 +0000 Subject: [PATCH 17/24] =?UTF-8?q?=F0=9F=A7=AA=20Sample=20classical=20adder?= =?UTF-8?q?s=20without=20unrolling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use direct DD execution for the classical phase tables, including zero, leading-zero, and carry cases. Assisted-by: GPT-6 via Codex --- test/python/test_bench.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/python/test_bench.py b/test/python/test_bench.py index fbbaed67c7..efed8c8fdc 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -214,11 +214,8 @@ def test_classical_qft_adder_reference_json_and_generation() -> None: def test_classical_qft_adder_dd_sampling_preserves_width_and_carry(addend: str, expected: str) -> None: """Execute zero, leading-zero, and carry cases against their exact sums.""" benchmark = qft_adder_classical.QFTAdderClassical(qft_adder_classical.Options(addend=addend)) - program = benchmark.generate().to_qco() - # Fold phase-table reads to scalars supported by the DD interpreter. - program.unroll_quantum_loops() shots = 1_024 - assert program.sample(shots=shots, seed=17) == {expected: shots} + assert benchmark.generate().to_qco().sample(shots=shots, seed=17) == {expected: shots} def test_qpe_accepts_fraction_and_native_phase() -> None: From a475fe4b7bfed711fbf8a19af66380e0b8010134 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:15:21 +0200 Subject: [PATCH 18/24] Fix classical QFT adder docstring markup Assisted-by: GPT-5.6 Sol via Codex --- bindings/bench/register_qft_adder_classical.cpp | 7 ++++--- python/mqt/core/bench/qft_adder_classical.pyi | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bindings/bench/register_qft_adder_classical.cpp b/bindings/bench/register_qft_adder_classical.cpp index 7670031eb6..0799d9ea2f 100644 --- a/bindings/bench/register_qft_adder_classical.cpp +++ b/bindings/bench/register_qft_adder_classical.cpp @@ -33,10 +33,11 @@ void registerQFTAdderClassical(const nb::module_& m) { auto qftAdder = nb::class_( m, "QFTAdderClassical", - R"pb(A QFT adder that adds the classical addend to an accumulator in :math:`|1\\rangle`. + R"pb(A QFT adder that adds the classical addend to an accumulator in :math:`|1\rangle`. -Leading zeros define the n-bit input width. The n+1-bit big-endian result is -addend + 1; the extra bit preserves carry. +Leading zeros define input width :math:`n`. For addend :math:`a`, the +big-endian result has :math:`n + 1` bits and equals :math:`a + 1`; the extra +bit preserves carry. Reference: https://arxiv.org/abs/quant-ph/0205095)pb"); qftAdder.def(nb::init(), "options"_a) diff --git a/python/mqt/core/bench/qft_adder_classical.pyi b/python/mqt/core/bench/qft_adder_classical.pyi index 6b05fd7940..70244d1584 100644 --- a/python/mqt/core/bench/qft_adder_classical.pyi +++ b/python/mqt/core/bench/qft_adder_classical.pyi @@ -22,10 +22,11 @@ class Options: """The big-endian classical addend.""" class QFTAdderClassical: - """A QFT adder that adds the classical addend to an accumulator in :math:`|1\\\\rangle`. + """A QFT adder that adds the classical addend to an accumulator in :math:`|1\\rangle`. - Leading zeros define the n-bit input width. The n+1-bit big-endian result is - addend + 1; the extra bit preserves carry. + Leading zeros define input width :math:`n`. For addend :math:`a`, the + big-endian result has :math:`n + 1` bits and equals :math:`a + 1`; the extra + bit preserves carry. Reference: https://arxiv.org/abs/quant-ph/0205095 """ From 44dbff49846113e0c5419c62c359873fc4dda508 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 13:40:45 +0000 Subject: [PATCH 19/24] =?UTF-8?q?=F0=9F=93=9D=20Compact=20constant-adder?= =?UTF-8?q?=20plan=20and=20sampling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the input, carry contract, and validation limits in one decision record. Assisted-by: GPT-6 via Codex --- .agent/plans/qft-adder-classical.md | 70 ++++++++++++----------------- test/python/test_bench.py | 2 +- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/.agent/plans/qft-adder-classical.md b/.agent/plans/qft-adder-classical.md index 0b7798347a..86b8160da3 100644 --- a/.agent/plans/qft-adder-classical.md +++ b/.agent/plans/qft-adder-classical.md @@ -1,49 +1,35 @@ -# Add a classical-input QFT adder benchmark +# Constant-input QFT adder benchmark -Status: complete. +Status: complete; configurable inputs and family consolidation remain proposals. ## Goal and scope -Add the `qft-adder-classical` structured benchmark from Beauregard's -[Circuit for Shor's algorithm using 2n+3 qubits](https://arxiv.org/abs/quant-ph/0205095), -Figure 3. Expose the benchmark through the typed C++, JSON, command-line, -Python, and MLIR generation interfaces. - -The instance parameter is a nonempty big-endian classical addend. Its length -defines an `n`-bit value. The benchmark prepares an `n+1`-qubit accumulator as -`|1>`, applies Beauregard's exact classical Fourier addition, and measures one -big-endian `result` output. The exact reference is the zero-extended addend plus -one; the extra accumulator qubit preserves overflow. +Expose `qft-adder-classical` through C++, Python, JSON, the CLI, and MLIR. +`src/bench/QFTAdderClassical.cpp` owns the reference; +`mlir/bench/programs/QFTAdderClassical.cpp` emits the Fourier constant adder. A +nonempty big-endian addend defines the input width `n`, including leading zeros. +The `n+1`-qubit accumulator starts in `|1>`; its measured result is the addend +plus one, including carry. ## Decisions -The generator uses the shared exact no-swap QFT and inverse-QFT helpers in -`mlir/bench/programs/QFTUtils.*`. Between them, it materializes the precomputed -angles as a dense tensor. A structured loop applies one unconditional phase gate -to each accumulator wire, including a zero-angle gate. For little-endian -accumulator wire `j`, the phase is the binary fraction formed by addend bits `j` -through zero. The extra wire receives the continued fraction and therefore -records carry. - -Compute the phase table by scanning the addend from least to most significant: -divide the previous angle by two and add pi for a set bit. Append one more -halved angle for the overflow wire. This produces canonical angles in -`[0, 2*pi)` without converting an arbitrary-width addend to a fixed-width -integer. The input length is limited to 1023 so the accumulator and QFT remain -within 1024 qubits. - -The fixed `|1>` accumulator is the benchmark harness, not part of the source's -general adder definition. Do not add swaps, approximate rotations, a carry -ancilla, or controlled phases: Beauregard's classical-input optimization -combines each wire's classically known rotations into one single-qubit phase. - -## Work completed - -- [x] Add the typed family, strict JSON contract, binding, stubs, and reference - tests. -- [x] Generate the complete Figure 3 circuit and test its phase table and - structured size. -- [x] Document the source, harness, bit order, phase convention, and output. -- [x] Validate the focused native, MLIR, and Python behavior. -- [x] Create the draft pull request on the quantum-input QFT-adder branch and - add its number to the rolling structured-benchmark changelog entry. +Reuse the no-swap QFT helpers. A known classical addend needs one precomputed +phase per accumulator wire, avoiding an extra quantum register and its +controlled gates. Scan bits from least to most significant, halving the previous +angle and adding pi for a set bit; append the halved angle for the carry wire. +This avoids fixed-width integer conversion. At most 1023 input bits keep the +accumulator within the shared 1024-qubit QFT limit. + +Dense f64 phase tables run directly in the DD interpreter. No unrolling is +needed. The fixed accumulator is a benchmark input, not a restriction of the +addition algorithm. Consolidation with the register-input family requires an +agreed overflow contract and configurable input semantics. + +## Validation + +Run `mqt-core-bench-test` and `mqt-core-mlir-unittests-benchmark` from their +build directories, and `uv run --no-sync pytest test/python/test_bench.py`. +Prior local checks passed for reference/JSON behavior, QC/jeff generation, phase +tables, and direct DD sampling of zero, leading-zero, and carry cases. These +tests cover the fixed accumulator; arbitrary input states remain outside the +benchmark contract. diff --git a/test/python/test_bench.py b/test/python/test_bench.py index efed8c8fdc..bda95c7da3 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -215,7 +215,7 @@ def test_classical_qft_adder_dd_sampling_preserves_width_and_carry(addend: str, """Execute zero, leading-zero, and carry cases against their exact sums.""" benchmark = qft_adder_classical.QFTAdderClassical(qft_adder_classical.Options(addend=addend)) shots = 1_024 - assert benchmark.generate().to_qco().sample(shots=shots, seed=17) == {expected: shots} + assert mlir.sample(benchmark.generate(), shots=shots, seed=17) == {expected: shots} def test_qpe_accepts_fraction_and_native_phase() -> None: From de8c218438ea91038519eb6b2892231749fbffd8 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 14:02:20 +0000 Subject: [PATCH 20/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Consolidate=20config?= =?UTF-8?q?urable=20QFT=20adders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use one family for register and constant operands with wrap or carry semantics. Preserve superposed register addends, share the generator and analytic reference, and remove duplicate public families. Assisted-by: GPT-6 via Codex --- .agent/plans/qft-adder-classical.md | 35 ---- .agent/plans/qft-adder-quantum.md | 34 ---- .agent/plans/qft-adder.md | 45 ++++ bindings/bench/CMakeLists.txt | 3 +- bindings/bench/register_bench.cpp | 16 +- bindings/bench/register_qft_adder.cpp | 114 +++++++++++ .../bench/register_qft_adder_classical.cpp | 95 --------- bindings/bench/register_qft_adder_quantum.cpp | 92 --------- docs/benchmarks.md | 30 +++ include/mqt-core/bench/BenchmarkFamilies.inc | 4 +- include/mqt-core/bench/JSON.hpp | 3 +- include/mqt-core/bench/QFTAdder.hpp | 65 ++++++ include/mqt-core/bench/QFTAdderClassical.hpp | 52 ----- include/mqt-core/bench/QFTAdderQuantum.hpp | 48 ----- mlir/bench/programs/CMakeLists.txt | 3 +- mlir/bench/programs/Programs.h | 13 +- mlir/bench/programs/QFTAdder.cpp | 152 ++++++++++++++ mlir/bench/programs/QFTAdderClassical.cpp | 79 ------- mlir/bench/programs/QFTAdderQuantum.cpp | 77 ------- mlir/include/mlir/bench/Generate.h | 11 +- mlir/unittests/bench/CMakeLists.txt | 3 +- mlir/unittests/bench/test_benchmark_cli.cmake | 4 +- .../bench/test_benchmark_generate.cpp | 12 +- ... => test_benchmark_generate_qft_adder.cpp} | 91 ++++++++- ...benchmark_generate_qft_adder_classical.cpp | 103 ---------- python/mqt/core/bench/__init__.pyi | 3 +- python/mqt/core/bench/qft_adder.pyi | 106 ++++++++++ python/mqt/core/bench/qft_adder_classical.pyi | 74 ------- python/mqt/core/bench/qft_adder_quantum.pyi | 70 ------- src/bench/JSON.cpp | 192 ++++++++++++------ src/bench/QFTAdder.cpp | 117 +++++++++++ src/bench/QFTAdderClassical.cpp | 78 ------- src/bench/QFTAdderQuantum.cpp | 72 ------- test/bench/test_json.cpp | 162 ++++++--------- test/bench/test_qft_adder.cpp | 118 +++++++++++ test/bench/test_qft_adder_classical.cpp | 78 ------- test/bench/test_qft_adder_quantum.cpp | 86 -------- test/python/test_bench.py | 106 +++++----- test/python/test_cli.py | 3 +- 39 files changed, 1100 insertions(+), 1349 deletions(-) delete mode 100644 .agent/plans/qft-adder-classical.md delete mode 100644 .agent/plans/qft-adder-quantum.md create mode 100644 .agent/plans/qft-adder.md create mode 100644 bindings/bench/register_qft_adder.cpp delete mode 100644 bindings/bench/register_qft_adder_classical.cpp delete mode 100644 bindings/bench/register_qft_adder_quantum.cpp create mode 100644 include/mqt-core/bench/QFTAdder.hpp delete mode 100644 include/mqt-core/bench/QFTAdderClassical.hpp delete mode 100644 include/mqt-core/bench/QFTAdderQuantum.hpp create mode 100644 mlir/bench/programs/QFTAdder.cpp delete mode 100644 mlir/bench/programs/QFTAdderClassical.cpp delete mode 100644 mlir/bench/programs/QFTAdderQuantum.cpp rename mlir/unittests/bench/{test_benchmark_generate_qft_adder_quantum.cpp => test_benchmark_generate_qft_adder.cpp} (58%) delete mode 100644 mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp create mode 100644 python/mqt/core/bench/qft_adder.pyi delete mode 100644 python/mqt/core/bench/qft_adder_classical.pyi delete mode 100644 python/mqt/core/bench/qft_adder_quantum.pyi create mode 100644 src/bench/QFTAdder.cpp delete mode 100644 src/bench/QFTAdderClassical.cpp delete mode 100644 src/bench/QFTAdderQuantum.cpp create mode 100644 test/bench/test_qft_adder.cpp delete mode 100644 test/bench/test_qft_adder_classical.cpp delete mode 100644 test/bench/test_qft_adder_quantum.cpp diff --git a/.agent/plans/qft-adder-classical.md b/.agent/plans/qft-adder-classical.md deleted file mode 100644 index 86b8160da3..0000000000 --- a/.agent/plans/qft-adder-classical.md +++ /dev/null @@ -1,35 +0,0 @@ -# Constant-input QFT adder benchmark - -Status: complete; configurable inputs and family consolidation remain proposals. - -## Goal and scope - -Expose `qft-adder-classical` through C++, Python, JSON, the CLI, and MLIR. -`src/bench/QFTAdderClassical.cpp` owns the reference; -`mlir/bench/programs/QFTAdderClassical.cpp` emits the Fourier constant adder. A -nonempty big-endian addend defines the input width `n`, including leading zeros. -The `n+1`-qubit accumulator starts in `|1>`; its measured result is the addend -plus one, including carry. - -## Decisions - -Reuse the no-swap QFT helpers. A known classical addend needs one precomputed -phase per accumulator wire, avoiding an extra quantum register and its -controlled gates. Scan bits from least to most significant, halving the previous -angle and adding pi for a set bit; append the halved angle for the carry wire. -This avoids fixed-width integer conversion. At most 1023 input bits keep the -accumulator within the shared 1024-qubit QFT limit. - -Dense f64 phase tables run directly in the DD interpreter. No unrolling is -needed. The fixed accumulator is a benchmark input, not a restriction of the -addition algorithm. Consolidation with the register-input family requires an -agreed overflow contract and configurable input semantics. - -## Validation - -Run `mqt-core-bench-test` and `mqt-core-mlir-unittests-benchmark` from their -build directories, and `uv run --no-sync pytest test/python/test_bench.py`. -Prior local checks passed for reference/JSON behavior, QC/jeff generation, phase -tables, and direct DD sampling of zero, leading-zero, and carry cases. These -tests cover the fixed accumulator; arbitrary input states remain outside the -benchmark contract. diff --git a/.agent/plans/qft-adder-quantum.md b/.agent/plans/qft-adder-quantum.md deleted file mode 100644 index 4482bc52ab..0000000000 --- a/.agent/plans/qft-adder-quantum.md +++ /dev/null @@ -1,34 +0,0 @@ -# Register-input QFT adder benchmark - -Status: complete; configurable inputs and family consolidation remain proposals. - -## Goal and scope - -Expose `qft-adder-quantum` through C++, Python, JSON, the CLI, and MLIR. -`src/bench/QFTAdderQuantum.cpp` owns its analytic reference; -`mlir/bench/programs/QFTAdderQuantum.cpp` emits Draper's controlled-phase adder. -The addend starts in `|+>^n`, the accumulator in `|1>`, and the result is the -big-endian concatenation `addend || sum`, with `sum = addend + 1 mod 2^n`. - -## Decisions - -Measure both registers: the sum alone is uniform and cannot check addition. The -shared no-swap QFT helpers in `mlir/bench/programs/QFTUtils.*` also serve QFT -and QPE. Keep controlled phase gates; controlled RZ changes relative phases. -Halve angles from the largest rotation to avoid premature underflow. The -1024-qubit register limit keeps the reference probability representable. - -The DD interpreter reads dense rank-one f64 phase tables with checked indices; -QPE and adder tests can sample structured programs without loop unrolling. The -name distinguishes a register-held addend from a classical constant, not quantum -computation from classical computation. Configurable operands and a shared -family need an agreed overflow contract before changing the public API. - -## Validation - -Run `mqt-core-bench-test` and `mqt-core-mlir-unittests-benchmark` from their -build directories, and `uv run --no-sync pytest test/python/test_bench.py`. -Prior local checks passed for reference/JSON behavior, QC/jeff generation, phase -structure, and DD sampling of the width-three correlated distribution. Sampling -this fixed input does not certify arbitrary accumulators or relative phases; -those require broader inputs and statevector or functionality checks. diff --git a/.agent/plans/qft-adder.md b/.agent/plans/qft-adder.md new file mode 100644 index 0000000000..fc7903ef47 --- /dev/null +++ b/.agent/plans/qft-adder.md @@ -0,0 +1,45 @@ +# QFT adder benchmark + +Status: in progress; validate the unified API and exhaustive small-width tests. + +## 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. + +## Work remaining + +- [ ] Validate options, references, JSON round trips, bindings, and QC/jeff + generation. +- [ ] Check every operand pair at widths one through three for both methods and + overflow policies. +- [ ] Check coherent addition amplitudes, then run required lint and stub + generation. + +## Validation + +Run the native benchmark and generation binaries and +`uv run --no-sync pytest test/python/test_bench.py test/python/test_cli.py -k bench`. +Local implementation checks passed: 47 native reference/JSON tests, 17 +generation tests, and 26 Python benchmark/CLI tests. Stubs were regenerated. The +implementation belongs to #2404; #2408 adds exhaustive arithmetic and +phase-sensitive execution checks. Both retain the existing PR chain. diff --git a/bindings/bench/CMakeLists.txt b/bindings/bench/CMakeLists.txt index e504b2c894..e5d58009eb 100644 --- a/bindings/bench/CMakeLists.txt +++ b/bindings/bench/CMakeLists.txt @@ -14,8 +14,7 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}-bench-bindings) register_grover.cpp register_multiplexer.cpp register_qft.cpp - register_qft_adder_classical.cpp - register_qft_adder_quantum.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 37d85f03ff..b48e7b77fd 100644 --- a/bindings/bench/register_bench.cpp +++ b/bindings/bench/register_bench.cpp @@ -24,8 +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 registerQFTAdderClassical(const nb::module_& m); -void registerQFTAdderQuantum(const nb::module_& m); +void registerQFTAdder(const nb::module_& m); void registerQPE(const nb::module_& m); void registerTeleportation(const nb::module_& m); @@ -71,16 +70,9 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { m.def_submodule("qft", "QFT benchmark instances and options."); registerQFT(qft); - const nb::module_ qftAdderClassical = - m.def_submodule("qft_adder_classical", - "Classical-input QFT adder benchmark instances and " - "options."); - registerQFTAdderClassical(qftAdderClassical); - - const nb::module_ qftAdderQuantum = m.def_submodule( - "qft_adder_quantum", - "Quantum-input QFT adder benchmark instances and options."); - registerQFTAdderQuantum(qftAdderQuantum); + 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."); diff --git a/bindings/bench/register_qft_adder.cpp b/bindings/bench/register_qft_adder.cpp new file mode 100644 index 0000000000..72e17797b2 --- /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, + "Big-endian addend; register inputs also accept '+' for a |+> qubit.") + .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/bindings/bench/register_qft_adder_classical.cpp b/bindings/bench/register_qft_adder_classical.cpp deleted file mode 100644 index 0799d9ea2f..0000000000 --- a/bindings/bench/register_qft_adder_classical.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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/QFTAdderClassical.hpp" - -#include -#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 registerQFTAdderClassical(const nb::module_& m) { - nb::class_( - m, "Options", "Parameters for a classical-input QFT adder benchmark.") - .def(nb::init(), nb::kw_only(), "addend"_a) - .def_ro("addend", &bench::QFTAdderClassicalOptions::addend, - "The big-endian classical addend."); - - auto qftAdder = nb::class_( - m, "QFTAdderClassical", - R"pb(A QFT adder that adds the classical addend to an accumulator in :math:`|1\rangle`. - -Leading zeros define input width :math:`n`. For addend :math:`a`, the -big-endian result has :math:`n + 1` bits and equals :math:`a + 1`; the extra -bit preserves carry. - -Reference: https://arxiv.org/abs/quant-ph/0205095)pb"); - qftAdder.def(nb::init(), "options"_a) - .def_prop_ro("options", &bench::QFTAdderClassical::options, - nb::rv_policy::reference_internal, - "The resolved benchmark parameters.") - .def_prop_ro("output", &bench::QFTAdderClassical::output, - nb::rv_policy::reference_internal, - "The logical result register.") - .def_prop_ro("expected_result", &bench::QFTAdderClassical::expectedResult, - nb::rv_policy::reference_internal, - "The deterministic big-endian result.") - .def("probability", &bench::QFTAdderClassical::probability, "outcome"_a, - "Return the ideal probability of an outcome.") - .def("evaluate", &bench::QFTAdderClassical::evaluate, "counts"_a, - "Compare sampled counts with the ideal distribution.") - .def( - "generate", - [](const bench::QFTAdderClassical& 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::QFTAdderClassical& value) { - return bench::toInstanceSpecificationJSON(value); - }, - "The canonical instance specification JSON.") - .def_prop_ro( - "manifest_json", - [](const bench::QFTAdderClassical& value) { - return bench::toManifestJSON(value); - }, - "The canonical manifest JSON.") - .def_prop_ro( - "case_id", - [](const bench::QFTAdderClassical& value) { - return bench::caseId(value); - }, - "The stable semantic case ID.") - .def_static("from_instance_specification_json", - &bench::qftAdderClassicalFromInstanceSpecificationJSON, - "json"_a, nb::kw_only(), - "source"_a = "", - "Parse a strict benchmark instance specification.") - .def_static("from_manifest_json", - &bench::qftAdderClassicalFromManifestJSON, "json"_a, - nb::kw_only(), "source"_a = "", - "Parse a strict benchmark manifest."); -} - -} // namespace mqt diff --git a/bindings/bench/register_qft_adder_quantum.cpp b/bindings/bench/register_qft_adder_quantum.cpp deleted file mode 100644 index 84ea7800db..0000000000 --- a/bindings/bench/register_qft_adder_quantum.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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/QFTAdderQuantum.hpp" - -#include -#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 registerQFTAdderQuantum(const nb::module_& m) { - nb::class_( - m, "Options", "Parameters for a quantum-input QFT adder benchmark.") - .def(nb::init(), nb::kw_only(), "qubits"_a) - .def_ro("qubits", &bench::QFTAdderQuantumOptions::qubits, - "The number of qubits in each input register."); - - auto qftAdder = nb::class_( - m, "QFTAdderQuantum", - R"pb(A QFT adder with an :math:`n`-qubit addend in :math:`|+\rangle^{\otimes n}` and an accumulator in :math:`|1\rangle`. - -Big-endian outcomes concatenate the addend and sum, each with :math:`n` bits. -For addend :math:`a`, the sum is :math:`(a + 1) \bmod 2^n`; each valid -outcome has probability :math:`2^{-n}`. - -Reference: https://arxiv.org/abs/quant-ph/0008033)pb"); - qftAdder.def(nb::init(), "options"_a) - .def_prop_ro("options", &bench::QFTAdderQuantum::options, - nb::rv_policy::reference_internal, - "The resolved benchmark parameters.") - .def_prop_ro( - "output", &bench::QFTAdderQuantum::output, - nb::rv_policy::reference_internal, - "The logical output register, with the addend followed by the sum.") - .def("probability", &bench::QFTAdderQuantum::probability, "outcome"_a, - "Return the ideal probability of an outcome.") - .def("evaluate", &bench::QFTAdderQuantum::evaluate, "counts"_a, - "Compare sampled counts with the ideal distribution.") - .def( - "generate", - [](const bench::QFTAdderQuantum& 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::QFTAdderQuantum& value) { - return bench::toInstanceSpecificationJSON(value); - }, - "The canonical instance specification JSON.") - .def_prop_ro( - "manifest_json", - [](const bench::QFTAdderQuantum& value) { - return bench::toManifestJSON(value); - }, - "The canonical manifest JSON.") - .def_prop_ro( - "case_id", - [](const bench::QFTAdderQuantum& value) { - return bench::caseId(value); - }, - "The stable semantic case ID.") - .def_static("from_instance_specification_json", - &bench::qftAdderQuantumFromInstanceSpecificationJSON, - "json"_a, nb::kw_only(), - "source"_a = "", - "Parse a strict benchmark instance specification.") - .def_static("from_manifest_json", &bench::qftAdderQuantumFromManifestJSON, - "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..144b5c3806 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -236,3 +236,33 @@ 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. + +## 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 1eb6324530..716647c7fc 100644 --- a/include/mqt-core/bench/BenchmarkFamilies.inc +++ b/include/mqt-core/bench/BenchmarkFamilies.inc @@ -30,9 +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(QFTAdderClassical, qftAdderClassical, - "qft-adder-classical", 1) -MQT_BENCHMARK_FAMILY(QFTAdderQuantum, qftAdderQuantum, "qft-adder-quantum", 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 ae62643bce..3e2351c8df 100644 --- a/include/mqt-core/bench/JSON.hpp +++ b/include/mqt-core/bench/JSON.hpp @@ -16,8 +16,7 @@ #include "bench/Grover.hpp" #include "bench/Multiplexer.hpp" #include "bench/QFT.hpp" -#include "bench/QFTAdderClassical.hpp" -#include "bench/QFTAdderQuantum.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/include/mqt-core/bench/QFTAdderClassical.hpp b/include/mqt-core/bench/QFTAdderClassical.hpp deleted file mode 100644 index c8981c90a8..0000000000 --- a/include/mqt-core/bench/QFTAdderClassical.hpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 - -namespace mqt::bench { - -/// Parameters for one classical-input QFT adder benchmark instance. -struct QFTAdderClassicalOptions { - static constexpr size_t MAX_ADDEND_BITS = 1'023; - - /// Big-endian classical addend. Leading zeros define its width. - std::string addend; -}; - -/// A QFT adder that adds the classical addend to an accumulator in |1>. -/// Leading zeros define the n-bit input width. The n+1-bit big-endian result -/// is addend + 1; the extra bit preserves carry. -class MQT_CORE_BENCH_EXPORT QFTAdderClassical final { -public: - explicit QFTAdderClassical(QFTAdderClassicalOptions options); - - [[nodiscard]] const QFTAdderClassicalOptions& options() const noexcept; - [[nodiscard]] const Output& output() const noexcept; - /// Return the deterministic big-endian result. - [[nodiscard]] const std::string& expectedResult() const noexcept; - /// Return the ideal probability of a big-endian 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: - QFTAdderClassicalOptions options_; - Output output_; - std::string expectedResult_; -}; - -} // namespace mqt::bench diff --git a/include/mqt-core/bench/QFTAdderQuantum.hpp b/include/mqt-core/bench/QFTAdderQuantum.hpp deleted file mode 100644 index 215fa3694d..0000000000 --- a/include/mqt-core/bench/QFTAdderQuantum.hpp +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 - -namespace mqt::bench { - -/// Parameters for one quantum-input QFT adder benchmark instance. -struct QFTAdderQuantumOptions { - static constexpr size_t MAX_QUBITS = 1'024; - - /// Number of qubits in each input register. - size_t qubits; -}; - -/// A QFT adder with an n-qubit addend in |+>^n and an accumulator in |1>. -/// Big-endian outcomes concatenate the addend and sum, each with n bits. -/// The sum is addend + 1 modulo 2^n; each valid outcome has probability 2^-n. -class MQT_CORE_BENCH_EXPORT QFTAdderQuantum final { -public: - explicit QFTAdderQuantum(QFTAdderQuantumOptions options); - - [[nodiscard]] const QFTAdderQuantumOptions& options() const noexcept; - [[nodiscard]] const Output& output() const noexcept; - /// Return the ideal probability of a big-endian 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: - QFTAdderQuantumOptions options_; - Output output_; -}; - -} // namespace mqt::bench diff --git a/mlir/bench/programs/CMakeLists.txt b/mlir/bench/programs/CMakeLists.txt index 7e89dcd76b..8fdd8a24b3 100644 --- a/mlir/bench/programs/CMakeLists.txt +++ b/mlir/bench/programs/CMakeLists.txt @@ -13,8 +13,7 @@ add_library( Grover.cpp Multiplexer.cpp QFT.cpp - QFTAdderClassical.cpp - QFTAdderQuantum.cpp + QFTAdder.cpp QFTUtils.cpp QPE.cpp Teleportation.cpp) diff --git a/mlir/bench/programs/Programs.h b/mlir/bench/programs/Programs.h index d26b660241..00f23daddb 100644 --- a/mlir/bench/programs/Programs.h +++ b/mlir/bench/programs/Programs.h @@ -23,8 +23,7 @@ class GHZ; class Grover; class Multiplexer; class QFT; -class QFTAdderClassical; -class QFTAdderQuantum; +class QFTAdder; class QPE; class Teleportation; } // namespace mqt::bench @@ -50,13 +49,9 @@ SmallVector multiplexer(qc::QCProgramBuilder& builder, /// Emit one configured QFT benchmark. SmallVector qft(qc::QCProgramBuilder& builder, const QFT& benchmark); -/// Emit one configured classical-input QFT adder benchmark. -SmallVector qftAdderClassical(qc::QCProgramBuilder& builder, - const QFTAdderClassical& benchmark); - -/// Emit one configured quantum-input QFT adder benchmark. -SmallVector qftAdderQuantum(qc::QCProgramBuilder& builder, - const QFTAdderQuantum& 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/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/QFTAdderClassical.cpp b/mlir/bench/programs/QFTAdderClassical.cpp deleted file mode 100644 index 5a1f01f4a8..0000000000 --- a/mlir/bench/programs/QFTAdderClassical.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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/QFTAdderClassical.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 - -namespace mqt::bench { - -using namespace mlir; - -[[nodiscard]] static Value phaseAngles(qc::QCProgramBuilder& builder, - const std::string_view addend) { - 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)); - } - 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 qftAdderClassical(qc::QCProgramBuilder& builder, - const QFTAdderClassical& benchmark) { - const auto qubits = - static_cast(benchmark.options().addend.size() + 1U); - auto sum = builder.allocQubitRegisterStorage(qubits, "sum"); - auto result = builder.allocClassicalBitRegister( - static_cast(benchmark.output().width), benchmark.output().name); - - auto zero = builder.indexConstant(0); - builder.x(builder.loadQubit(sum, zero)); - - detail::forwardQFT(builder, sum, qubits); - auto angles = phaseAngles(builder, benchmark.options().addend); - builder.scfFor(0, qubits, 1, [&](Value target) { - auto angle = tensor::ExtractOp::create(builder, angles, ValueRange{target}) - .getResult(); - builder.p(angle, builder.loadQubit(sum, target)); - }); - detail::inverseQFT(builder, sum, qubits); - - builder.measureQubitRegister(sum, result, qubits); - return {result}; -} - -} // namespace mqt::bench diff --git a/mlir/bench/programs/QFTAdderQuantum.cpp b/mlir/bench/programs/QFTAdderQuantum.cpp deleted file mode 100644 index d9476ebd88..0000000000 --- a/mlir/bench/programs/QFTAdderQuantum.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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/QFTAdderQuantum.hpp" - -#include "Programs.h" -#include "QFTUtils.h" -#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" - -#include -#include -#include - -#include -#include - -namespace mqt::bench { - -using namespace mlir; - -static void addQuantumRegister(qc::QCProgramBuilder& builder, Value addend, - Value sum, 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); - 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)); - }); - }); -} - -SmallVector qftAdderQuantum(qc::QCProgramBuilder& builder, - const QFTAdderQuantum& benchmark) { - const auto qubits = static_cast(benchmark.options().qubits); - auto addend = builder.allocQubitRegisterStorage(qubits, "addend"); - auto sum = builder.allocQubitRegisterStorage(qubits, "sum"); - auto result = builder.allocClassicalBitRegister( - static_cast(benchmark.output().width), benchmark.output().name); - - builder.scfFor(0, qubits, 1, [&](Value index) { - builder.h(builder.loadQubit(addend, index)); - }); - auto zero = builder.indexConstant(0); - builder.x(builder.loadQubit(sum, zero)); - - detail::forwardQFT(builder, sum, qubits); - addQuantumRegister(builder, addend, sum, qubits); - detail::inverseQFT(builder, sum, qubits); - - builder.measureQubitRegister(sum, result, qubits); - auto resultOffset = builder.indexConstant(qubits); - builder.scfFor(0, qubits, 1, [&](Value index) { - auto resultIndex = - arith::AddIOp::create(builder, resultOffset, index).getResult(); - builder.measure(builder.loadQubit(addend, index), result, resultIndex); - }); - return {result}; -} - -} // namespace mqt::bench diff --git a/mlir/include/mlir/bench/Generate.h b/mlir/include/mlir/bench/Generate.h index 8fa3e459f1..f8c865fcff 100644 --- a/mlir/include/mlir/bench/Generate.h +++ b/mlir/include/mlir/bench/Generate.h @@ -22,8 +22,7 @@ class GHZ; class Grover; class Multiplexer; class QFT; -class QFTAdderClassical; -class QFTAdderQuantum; +class QFTAdder; class QPE; class Teleportation; @@ -51,13 +50,9 @@ generate(const Multiplexer& benchmark); /// Generate a configured quantum Fourier-transform benchmark. [[nodiscard]] std::optional generate(const QFT& benchmark); -/// Generate a configured classical-input QFT adder benchmark. +/// Generate a configured QFT adder benchmark. [[nodiscard]] std::optional -generate(const QFTAdderClassical& benchmark); - -/// Generate a configured quantum-input QFT adder benchmark. -[[nodiscard]] std::optional -generate(const QFTAdderQuantum& benchmark); +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 21381654cf..2462ed92bc 100644 --- a/mlir/unittests/bench/CMakeLists.txt +++ b/mlir/unittests/bench/CMakeLists.txt @@ -14,8 +14,7 @@ add_executable( test_benchmark_generate_grover.cpp test_benchmark_generate_multiplexer.cpp test_benchmark_generate_qft.cpp - test_benchmark_generate_qft_adder_classical.cpp - test_benchmark_generate_qft_adder_quantum.cpp + test_benchmark_generate_qft_adder.cpp test_benchmark_generate_qpe.cpp test_benchmark_generate_teleportation.cpp) diff --git a/mlir/unittests/bench/test_benchmark_cli.cmake b/mlir/unittests/bench/test_benchmark_cli.cmake index 3844278d0e..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 9) - message(FATAL_ERROR "list returned ${benchmark_count} benchmarks instead of 9") +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 00f9b44f24..a6199f743a 100644 --- a/mlir/unittests/bench/test_benchmark_generate.cpp +++ b/mlir/unittests/bench/test_benchmark_generate.cpp @@ -14,8 +14,7 @@ #include "bench/Grover.hpp" #include "bench/Multiplexer.hpp" #include "bench/QFT.hpp" -#include "bench/QFTAdderClassical.hpp" -#include "bench/QFTAdderQuantum.hpp" +#include "bench/QFTAdder.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" #include "mlir/bench/Generate.h" @@ -46,8 +45,13 @@ TEST(GenerateProgramTest, GeneratesEveryBenchmarkMethodAsQCAndJeff) { expectValidQCAndJeff(QFT{{.qubits = 3, .periodExponent = 1}}); expectValidQCAndJeff(QFT{ {.qubits = 3, .periodExponent = 1, .method = QFTMethod::Semiclassical}}); - expectValidQCAndJeff(QFTAdderClassical{{.addend = "101"}}); - expectValidQCAndJeff(QFTAdderQuantum{{.qubits = 3}}); + 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_quantum.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp similarity index 58% rename from mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp rename to mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp index 6e466de469..853881a6a4 100644 --- a/mlir/unittests/bench/test_benchmark_generate_qft_adder_quantum.cpp +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp @@ -9,15 +9,17 @@ */ #include "TestUtils.h" -#include "bench/QFTAdderQuantum.hpp" +#include "bench/QFTAdder.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 @@ -26,6 +28,8 @@ #include #include #include +#include +#include namespace mqt::bench { @@ -47,7 +51,7 @@ static void expectConstantFloat(Value value, double expected) { TEST(GenerateProgramTest, EmitsQuantumQFTAdderCircuit) { constexpr int64_t qubits = 3; - auto program = generate(QFTAdderQuantum{{.qubits = qubits}}); + auto program = generate(QFTAdder{{.addend = "+++", .accumulator = "001"}}); ASSERT_TRUE(program); auto moduleOp = program->module(); @@ -60,7 +64,7 @@ TEST(GenerateProgramTest, EmitsQuantumQFTAdderCircuit) { EXPECT_EQ(test::countOps(moduleOp), 2U); EXPECT_EQ(test::countOps(moduleOp), 0U); - // Unlike the QFT phases, the addition phase connects the two registers. + /// 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(); @@ -118,8 +122,10 @@ TEST(GenerateProgramTest, EmitsQuantumQFTAdderCircuit) { } TEST(GenerateProgramTest, KeepsLargestQuantumQFTAdderFiniteAndStructured) { - auto program = - generate(QFTAdderQuantum{{.qubits = QFTAdderQuantumOptions::MAX_QUBITS}}); + 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(); @@ -131,4 +137,79 @@ TEST(GenerateProgramTest, KeepsLargestQuantumQFTAdderFiniteAndStructured) { }); } +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); +} + } // namespace mqt::bench diff --git a/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp deleted file mode 100644 index 20d1c11b4b..0000000000 --- a/mlir/unittests/bench/test_benchmark_generate_qft_adder_classical.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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/QFTAdderClassical.hpp" -#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 - -namespace mqt::bench { - -using namespace mlir; - -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(QFTAdderClassical{{.addend = "101"}}); - 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(QFTAdderClassicalOptions::MAX_ADDEND_BITS, '1'); - auto program = generate(QFTAdderClassical{{.addend = std::move(addend)}}); - ASSERT_TRUE(program); - auto moduleOp = program->module(); - - auto table = test::angleTable(moduleOp); - ASSERT_TRUE(table); - EXPECT_EQ(table.getNumElements(), - QFTAdderClassicalOptions::MAX_ADDEND_BITS + 1U); - for (const auto angle : table.getValues()) { - EXPECT_TRUE(std::isfinite(angle)); - } - - EXPECT_EQ(test::countOps(moduleOp), 1U); - EXPECT_LT(test::countOperations(moduleOp), 100U); -} - -} // namespace mqt::bench diff --git a/python/mqt/core/bench/__init__.pyi b/python/mqt/core/bench/__init__.pyi index 02f56d2483..dbf805b779 100644 --- a/python/mqt/core/bench/__init__.pyi +++ b/python/mqt/core/bench/__init__.pyi @@ -13,8 +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_classical as qft_adder_classical -from mqt.core.bench import qft_adder_quantum as qft_adder_quantum +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..b5411f408b --- /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 |+> 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/python/mqt/core/bench/qft_adder_classical.pyi b/python/mqt/core/bench/qft_adder_classical.pyi deleted file mode 100644 index 70244d1584..0000000000 --- a/python/mqt/core/bench/qft_adder_classical.pyi +++ /dev/null @@ -1,74 +0,0 @@ -# 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 - -"""Classical-input QFT adder benchmark instances and options.""" - -from collections.abc import Mapping - -import mqt.core.bench -import mqt.core.mlir - -class Options: - """Parameters for a classical-input QFT adder benchmark.""" - - def __init__(self, *, addend: str) -> None: ... - @property - def addend(self) -> str: - """The big-endian classical addend.""" - -class QFTAdderClassical: - """A QFT adder that adds the classical addend to an accumulator in :math:`|1\\rangle`. - - Leading zeros define input width :math:`n`. For addend :math:`a`, the - big-endian result has :math:`n + 1` bits and equals :math:`a + 1`; the extra - bit preserves carry. - - Reference: https://arxiv.org/abs/quant-ph/0205095 - """ - - 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 result register.""" - - @property - def expected_result(self) -> str: - """The deterministic big-endian result.""" - - 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 = "") -> QFTAdderClassical: - """Parse a strict benchmark instance specification.""" - - @staticmethod - def from_manifest_json(json: str, *, source: str = "") -> QFTAdderClassical: - """Parse a strict benchmark manifest.""" diff --git a/python/mqt/core/bench/qft_adder_quantum.pyi b/python/mqt/core/bench/qft_adder_quantum.pyi deleted file mode 100644 index 9ce6d9264e..0000000000 --- a/python/mqt/core/bench/qft_adder_quantum.pyi +++ /dev/null @@ -1,70 +0,0 @@ -# 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 - -"""Quantum-input QFT adder benchmark instances and options.""" - -from collections.abc import Mapping - -import mqt.core.bench -import mqt.core.mlir - -class Options: - """Parameters for a quantum-input QFT adder benchmark.""" - - def __init__(self, *, qubits: int) -> None: ... - @property - def qubits(self) -> int: - """The number of qubits in each input register.""" - -class QFTAdderQuantum: - """A QFT adder with an :math:`n`-qubit addend in :math:`|+\\rangle^{\\otimes n}` and an accumulator in :math:`|1\\rangle`. - - Big-endian outcomes concatenate the addend and sum, each with :math:`n` bits. - For addend :math:`a`, the sum is :math:`(a + 1) \\bmod 2^n`; each valid - outcome has probability :math:`2^{-n}`. - - Reference: https://arxiv.org/abs/quant-ph/0008033 - """ - - 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, with the addend followed by the sum.""" - - 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 = "") -> QFTAdderQuantum: - """Parse a strict benchmark instance specification.""" - - @staticmethod - def from_manifest_json(json: str, *, source: str = "") -> QFTAdderQuantum: - """Parse a strict benchmark manifest.""" diff --git a/src/bench/JSON.cpp b/src/bench/JSON.cpp index a228eccf2b..6f02a31f87 100644 --- a/src/bench/JSON.cpp +++ b/src/bench/JSON.cpp @@ -17,8 +17,7 @@ #include "bench/Grover.hpp" #include "bench/Multiplexer.hpp" #include "bench/QFT.hpp" -#include "bench/QFTAdderClassical.hpp" -#include "bench/QFTAdderQuantum.hpp" +#include "bench/QFTAdder.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -425,31 +424,40 @@ parseMultiplexerParameters(const Json& parameters, } } -[[nodiscard]] QFTAdderClassical -parseQFTAdderClassicalParameters(const Json& parameters, - const std::string_view source) { - rejectUnknownKeys(parameters, {"addend"}, source, "$/parameters"); - try { - return QFTAdderClassical({ - .addend = - stringValue(required(parameters, "addend", source, "$/parameters"), - source, "$/parameters/addend"), - }); - } catch (const std::invalid_argument& error) { - fail(source, "$/parameters", error.what()); +[[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'"); + } } -} - -[[nodiscard]] QFTAdderQuantum -parseQFTAdderQuantumParameters(const Json& parameters, - const std::string_view source) { - rejectUnknownKeys(parameters, {"qubits"}, source, "$/parameters"); try { - return QFTAdderQuantum({ - .qubits = - sizeValue(required(parameters, "qubits", source, "$/parameters"), - source, "$/parameters/qubits"), - }); + return QFTAdder(std::move(options)); } catch (const std::invalid_argument& error) { fail(source, "$/parameters", error.what()); } @@ -559,12 +567,20 @@ parseTeleportationParameters(const Json& parameters, }; } -[[nodiscard]] Json parametersJSON(const QFTAdderClassical& benchmark) { - return {{"addend", benchmark.options().addend}}; -} - -[[nodiscard]] Json parametersJSON(const QFTAdderQuantum& benchmark) { - return {{"qubits", benchmark.options().qubits}}; +[[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) { @@ -638,25 +654,18 @@ parseTeleportationParameters(const Json& parameters, }; } -[[nodiscard]] Json referenceJSON(const QFTAdderClassical& benchmark) { - return { - {"kind", "analytic"}, - {"model", "qft_adder_classical"}, - {"outcome_order", "big_endian"}, - {"output", benchmark.output().name}, - {"success_outcome", benchmark.expectedResult()}, - {"version", 1}, - }; -} - -[[nodiscard]] Json referenceJSON(const QFTAdderQuantum& benchmark) { - return { +[[nodiscard]] Json referenceJSON(const QFTAdder& benchmark) { + Json reference = { {"kind", "analytic"}, - {"model", "qft_adder_quantum"}, + {"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) { @@ -937,8 +946,8 @@ template }); } -[[nodiscard]] Json qftAdderClassicalInstanceSpecificationSchema() { - return baseInstanceSpecificationSchema({ +[[nodiscard]] Json qftAdderInstanceSpecificationSchema() { + return baseInstanceSpecificationSchema({ {"additionalProperties", false}, { "properties", @@ -946,36 +955,95 @@ template { "addend", { - {"maxLength", QFTAdderClassicalOptions::MAX_ADDEND_BITS}, + {"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"}, }, }, }, }, - {"required", {"addend"}}, - {"type", "object"}, - }); -} - -[[nodiscard]] Json qftAdderQuantumInstanceSpecificationSchema() { - return baseInstanceSpecificationSchema({ - {"additionalProperties", false}, { - "properties", + "allOf", { { - "qubits", { - {"maximum", QFTAdderQuantumOptions::MAX_QUBITS}, - {"minimum", 1}, - {"type", "integer"}, + "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", {"qubits"}}, + {"required", {"addend", "accumulator"}}, {"type", "object"}, }); } 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/src/bench/QFTAdderClassical.cpp b/src/bench/QFTAdderClassical.cpp deleted file mode 100644 index c636f048dd..0000000000 --- a/src/bench/QFTAdderClassical.cpp +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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/QFTAdderClassical.hpp" - -#include "EvaluationUtils.hpp" -#include "bench/Evaluation.hpp" - -#include -#include -#include -#include -#include -#include - -namespace mqt::bench { -namespace { - -[[nodiscard]] std::string increment(const std::string_view addend) { - auto result = std::string{"0"} + std::string{addend}; - auto carry = true; - for (size_t index = result.size(); index > 0 && carry; --index) { - auto& bit = result[index - 1]; - carry = bit == '1'; - bit = carry ? '0' : '1'; - } - return result; -} - -} // namespace - -QFTAdderClassical::QFTAdderClassical(QFTAdderClassicalOptions options) - : options_(std::move(options)), - output_{.name = "result", .width = options_.addend.size() + 1U} { - const auto width = options_.addend.size(); - if (width == 0 || width > QFTAdderClassicalOptions::MAX_ADDEND_BITS) { - throw std::invalid_argument( - "classical QFT adder addend must contain between 1 and 1023 bits"); - } - if (!std::ranges::all_of(options_.addend, [](const char bit) { - return bit == '0' || bit == '1'; - })) { - throw std::invalid_argument( - "classical QFT adder addend must contain only '0' and '1'"); - } - expectedResult_ = increment(options_.addend); -} - -const QFTAdderClassicalOptions& QFTAdderClassical::options() const noexcept { - return options_; -} - -const Output& QFTAdderClassical::output() const noexcept { return output_; } - -const std::string& QFTAdderClassical::expectedResult() const noexcept { - return expectedResult_; -} - -double QFTAdderClassical::probability(const std::string_view outcome) const { - detail::validateOutcome(outcome, output_.width); - return outcome == expectedResult_ ? 1. : 0.; -} - -Evaluation QFTAdderClassical::evaluate(const Counts& counts) const { - return detail::evaluate( - output_, counts, - [this](const std::string_view outcome) { return probability(outcome); }, - expectedResult_); -} - -} // namespace mqt::bench diff --git a/src/bench/QFTAdderQuantum.cpp b/src/bench/QFTAdderQuantum.cpp deleted file mode 100644 index f50940ed8a..0000000000 --- a/src/bench/QFTAdderQuantum.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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/QFTAdderQuantum.hpp" - -#include "EvaluationUtils.hpp" -#include "bench/Evaluation.hpp" - -#include -#include -#include -#include - -namespace mqt::bench { -namespace { - -[[nodiscard]] bool isIncrement(const std::string_view addend, - const std::string_view sum) { - auto carry = true; - for (size_t index = addend.size(); index > 0; --index) { - const auto addendBit = addend[index - 1] == '1'; - const auto expectedSumBit = addendBit != carry; - if ((sum[index - 1] == '1') != expectedSumBit) { - return false; - } - carry = addendBit && carry; - } - return true; -} - -} // namespace - -QFTAdderQuantum::QFTAdderQuantum(QFTAdderQuantumOptions options) - : options_(options), - output_{.name = "result", .width = 2 * options_.qubits} { - if (options_.qubits == 0 || - options_.qubits > QFTAdderQuantumOptions::MAX_QUBITS) { - throw std::invalid_argument( - "quantum QFT adder qubits must be between 1 and 1024"); - } -} - -const QFTAdderQuantumOptions& QFTAdderQuantum::options() const noexcept { - return options_; -} - -const Output& QFTAdderQuantum::output() const noexcept { return output_; } - -double QFTAdderQuantum::probability(const std::string_view outcome) const { - detail::validateOutcome(outcome, output_.width); - const auto addend = outcome.substr(0, options_.qubits); - const auto sum = outcome.substr(options_.qubits); - if (!isIncrement(addend, sum)) { - return 0.; - } - return std::ldexp(1., -static_cast(options_.qubits)); -} - -Evaluation QFTAdderQuantum::evaluate(const Counts& counts) const { - return detail::evaluate( - output_, counts, - [this](const std::string_view outcome) { return probability(outcome); }); -} - -} // namespace mqt::bench diff --git a/test/bench/test_json.cpp b/test/bench/test_json.cpp index 763d18e6c5..b5eb435735 100644 --- a/test/bench/test_json.cpp +++ b/test/bench/test_json.cpp @@ -15,8 +15,7 @@ #include "bench/JSON.hpp" #include "bench/Multiplexer.hpp" #include "bench/QFT.hpp" -#include "bench/QFTAdderClassical.hpp" -#include "bench/QFTAdderQuantum.hpp" +#include "bench/QFTAdder.hpp" #include "bench/QPE.hpp" #include "bench/Teleportation.hpp" @@ -58,12 +57,11 @@ using mqt::bench::multiplexerFromInstanceSpecificationJSON; using mqt::bench::multiplexerFromManifestJSON; using mqt::bench::Phase; using mqt::bench::QFT; -using mqt::bench::QFTAdderClassical; -using mqt::bench::qftAdderClassicalFromInstanceSpecificationJSON; -using mqt::bench::qftAdderClassicalFromManifestJSON; -using mqt::bench::QFTAdderQuantum; -using mqt::bench::qftAdderQuantumFromInstanceSpecificationJSON; -using mqt::bench::qftAdderQuantumFromManifestJSON; +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; @@ -127,20 +125,13 @@ TEST(BenchmarkJSON, toInstanceSpecificationJSON(qft), R"({"benchmark":"qft","parameters":{"method":"standard","period_exponent":2,"qubits":4},"schema_version":1})"); - const auto qftAdderClassical = qftAdderClassicalFromInstanceSpecificationJSON( - R"({"schema_version":1,"benchmark":"qft-adder-classical","parameters":{"addend":"001"}})"); - EXPECT_EQ(qftAdderClassical.options().addend, "001"); - EXPECT_EQ(qftAdderClassical.expectedResult(), "0010"); + 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(qftAdderClassical), - R"({"benchmark":"qft-adder-classical","parameters":{"addend":"001"},"schema_version":1})"); - - const auto qftAdderQuantum = qftAdderQuantumFromInstanceSpecificationJSON( - R"({"schema_version":1,"benchmark":"qft-adder-quantum","parameters":{"qubits":3}})"); - EXPECT_EQ(qftAdderQuantum.options().qubits, 3); - EXPECT_EQ( - toInstanceSpecificationJSON(qftAdderQuantum), - R"({"benchmark":"qft-adder-quantum","parameters":{"qubits":3},"schema_version":1})"); + 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"}})"); @@ -165,8 +156,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { const Multiplexer multiplexer{{.qubits = 7}}; const QFT qft{ {.qubits = 4, .periodExponent = 2, .method = QFTMethod::Semiclassical}}; - const QFTAdderClassical qftAdderClassical{{.addend = "110"}}; - const QFTAdderQuantum qftAdderQuantum{{.qubits = 3}}; + const QFTAdder qftAdder{{.addend = "+++", .accumulator = "001"}}; const QPE qpe{ {.precision = 5, .phase = Phase(1, 3), .method = QPEMethod::Iterative}}; const Teleportation teleportation; @@ -176,8 +166,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { const auto groverManifest = toManifestJSON(grover); const auto multiplexerManifest = toManifestJSON(multiplexer); const auto qftManifest = toManifestJSON(qft); - const auto qftAdderClassicalManifest = toManifestJSON(qftAdderClassical); - const auto qftAdderQuantumManifest = toManifestJSON(qftAdderQuantum); + const auto qftAdderManifest = toManifestJSON(qftAdder); const auto qpeManifest = toManifestJSON(qpe); const auto teleportationManifest = toManifestJSON(teleportation); EXPECT_EQ(toManifestJSON(bvFromManifestJSON(bvManifest)), bvManifest); @@ -187,12 +176,8 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { EXPECT_EQ(toManifestJSON(multiplexerFromManifestJSON(multiplexerManifest)), multiplexerManifest); EXPECT_EQ(toManifestJSON(qftFromManifestJSON(qftManifest)), qftManifest); - EXPECT_EQ(toManifestJSON( - qftAdderClassicalFromManifestJSON(qftAdderClassicalManifest)), - qftAdderClassicalManifest); - EXPECT_EQ( - toManifestJSON(qftAdderQuantumFromManifestJSON(qftAdderQuantumManifest)), - qftAdderQuantumManifest); + EXPECT_EQ(toManifestJSON(qftAdderFromManifestJSON(qftAdderManifest)), + qftAdderManifest); EXPECT_EQ(toManifestJSON(qpeFromManifestJSON(qpeManifest)), qpeManifest); EXPECT_EQ( toManifestJSON(teleportationFromManifestJSON(teleportationManifest)), @@ -202,10 +187,7 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { EXPECT_EQ(benchmarkIdFromManifestJSON(groverManifest), "grover"); EXPECT_EQ(benchmarkIdFromManifestJSON(multiplexerManifest), "multiplexer"); EXPECT_EQ(benchmarkIdFromManifestJSON(qftManifest), "qft"); - EXPECT_EQ(benchmarkIdFromManifestJSON(qftAdderClassicalManifest), - "qft-adder-classical"); - EXPECT_EQ(benchmarkIdFromManifestJSON(qftAdderQuantumManifest), - "qft-adder-quantum"); + EXPECT_EQ(benchmarkIdFromManifestJSON(qftAdderManifest), "qft-adder"); EXPECT_EQ(benchmarkIdFromManifestJSON(qpeManifest), "qpe"); EXPECT_EQ(benchmarkIdFromManifestJSON(teleportationManifest), "teleportation"); @@ -215,14 +197,9 @@ TEST(BenchmarkJSON, RoundTripsSelfCheckingManifests) { std::string::npos); EXPECT_NE(multiplexerManifest.find("\"model\":\"multiplexer\""), std::string::npos); - EXPECT_NE(qftAdderClassicalManifest.find("\"model\":\"qft_adder_classical\""), + EXPECT_NE(qftAdderManifest.find("\"model\":\"qft_adder\""), std::string::npos); - EXPECT_NE(qftAdderClassicalManifest.find("\"success_outcome\":\"0111\""), - std::string::npos); - EXPECT_NE(qftAdderClassicalManifest.find("\"width\":4"), std::string::npos); - EXPECT_NE(qftAdderQuantumManifest.find("\"model\":\"qft_adder_quantum\""), - std::string::npos); - EXPECT_NE(qftAdderQuantumManifest.find("\"width\":6"), 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); @@ -241,14 +218,10 @@ TEST(BenchmarkJSON, UsesStableSemanticCaseIds) { caseId(QFT{{.qubits = 3, .periodExponent = 1, .method = QFTMethod::Semiclassical}})); - EXPECT_EQ(caseId(QFTAdderClassical{{.addend = "001"}}), - caseId(QFTAdderClassical{{.addend = "001"}})); - EXPECT_NE(caseId(QFTAdderClassical{{.addend = "001"}}), - caseId(QFTAdderClassical{{.addend = "1"}})); - EXPECT_EQ(caseId(QFTAdderQuantum{{.qubits = 3}}), - caseId(QFTAdderQuantum{{.qubits = 3}})); - EXPECT_NE(caseId(QFTAdderQuantum{{.qubits = 3}}), - caseId(QFTAdderQuantum{{.qubits = 4}})); + 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}}), @@ -328,36 +301,24 @@ TEST(BenchmarkJSON, R"({"schema_version":1,"benchmark":"multiplexer","parameters":{"qubits":7,"angles":[]}})")); }, "unknown key 'angles'"); - expectInvalid( - [] { - static_cast(qftAdderClassicalFromInstanceSpecificationJSON( - R"({"schema_version":1,"benchmark":"qft-adder-classical","parameters":{"addend":""}})")); - }, - "between 1 and 1023 bits"); - expectInvalid( - [] { - static_cast(qftAdderClassicalFromInstanceSpecificationJSON( - R"({"schema_version":1,"benchmark":"qft-adder-classical","parameters":{"addend":"01x"}})")); - }, - "only '0' and '1'"); - expectInvalid( - [] { - static_cast(qftAdderClassicalFromInstanceSpecificationJSON( - R"({"schema_version":1,"benchmark":"qft-adder-classical","parameters":{"addend":"1","qubits":2}})")); - }, - "unknown key 'qubits'"); - expectInvalid( - [] { - static_cast(qftAdderQuantumFromInstanceSpecificationJSON( - R"({"schema_version":1,"benchmark":"qft-adder-quantum","parameters":{"qubits":0}})")); - }, - "between 1 and 1024"); - expectInvalid( - [] { - static_cast(qftAdderQuantumFromInstanceSpecificationJSON( - R"({"schema_version":1,"benchmark":"qft-adder-quantum","parameters":{"qubits":3,"addend":"1"}})")); - }, - "unknown key 'addend'"); + 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( @@ -424,14 +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":"qft-adder-classical"},{"definition_version":1,"id":"qft-adder-quantum"},{"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 qftAdderClassical = describeBenchmarkJSON("qft-adder-classical"); - const auto qftAdderQuantum = describeBenchmarkJSON("qft-adder-quantum"); + 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"), @@ -444,11 +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(qftAdderClassical.find("\"maxLength\":1023"), std::string::npos); - EXPECT_NE(qftAdderClassical.find("\"pattern\":\"^[01]+$\""), - std::string::npos); - EXPECT_NE(qftAdderQuantum.find("\"maximum\":1024"), std::string::npos); - EXPECT_NE(qftAdderQuantum.find("\"minimum\":1"), 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( @@ -487,23 +444,30 @@ TEST(BenchmarkJSON, ParsesCountsAndSerializesEvaluations) { EXPECT_NE(multiplexerEvaluation.find("\"total_variation_distance\":"), std::string::npos); - const QFTAdderQuantum qftAdderQuantum{{.qubits = 2}}; - const auto qftAdderQuantumEvaluation = evaluateJSON( - toManifestJSON(qftAdderQuantum), + 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(qftAdderQuantumEvaluation.find("\"success_probability\":null"), + EXPECT_NE(qftAdderEvaluation.find("\"success_probability\":null"), std::string::npos); - EXPECT_NE(qftAdderQuantumEvaluation.find("\"total_variation_distance\":0.0"), + EXPECT_NE(qftAdderEvaluation.find("\"total_variation_distance\":0.0"), std::string::npos); - const QFTAdderClassical qftAdderClassical{{.addend = "110"}}; - const auto qftAdderClassicalEvaluation = - evaluateJSON(toManifestJSON(qftAdderClassical), + 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(qftAdderClassicalEvaluation.find("\"success_probability\":0.8"), - std::string::npos); - EXPECT_NE(qftAdderClassicalEvaluation.find("\"total_variation_distance\":"), + 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 = 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/bench/test_qft_adder_classical.cpp b/test/bench/test_qft_adder_classical.cpp deleted file mode 100644 index c3ff455cdd..0000000000 --- a/test/bench/test_qft_adder_classical.cpp +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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/Evaluation.hpp" -#include "bench/QFTAdderClassical.hpp" - -#include - -#include -#include - -namespace { - -using mqt::bench::Output; -using mqt::bench::QFTAdderClassical; -using mqt::bench::QFTAdderClassicalOptions; - -TEST(QFTAdderClassical, StoresTheAddendAndResult) { - const QFTAdderClassical benchmark{{.addend = "00101"}}; - EXPECT_EQ(benchmark.options().addend, "00101"); - EXPECT_EQ(benchmark.output(), (Output{"result", 6})); - EXPECT_EQ(benchmark.expectedResult(), "000110"); -} - -TEST(QFTAdderClassical, ValidatesTheConfiguredInstance) { - const auto maximum = - std::string(QFTAdderClassicalOptions::MAX_ADDEND_BITS, '1'); - const auto tooLong = - std::string(QFTAdderClassicalOptions::MAX_ADDEND_BITS + 1U, '0'); - const QFTAdderClassical maximumBenchmark{{.addend = maximum}}; - EXPECT_THROW(static_cast(QFTAdderClassical{{.addend = ""}}), - std::invalid_argument); - EXPECT_THROW(static_cast(QFTAdderClassical{{.addend = "10x"}}), - std::invalid_argument); - EXPECT_EQ(maximumBenchmark.expectedResult(), - "1" + std::string(QFTAdderClassicalOptions::MAX_ADDEND_BITS, '0')); - EXPECT_THROW(static_cast(QFTAdderClassical{{.addend = tooLong}}), - std::invalid_argument); -} - -TEST(QFTAdderClassical, AddsOneWithoutTruncatingOverflow) { - const QFTAdderClassical zero{{.addend = "0"}}; - const QFTAdderClassical one{{.addend = "1"}}; - const QFTAdderClassical leadingZeros{{.addend = "001"}}; - const QFTAdderClassical five{{.addend = "101"}}; - const QFTAdderClassical six{{.addend = "110"}}; - const QFTAdderClassical seven{{.addend = "111"}}; - - EXPECT_DOUBLE_EQ(zero.probability("01"), 1.); - EXPECT_DOUBLE_EQ(one.probability("10"), 1.); - EXPECT_DOUBLE_EQ(leadingZeros.probability("0010"), 1.); - EXPECT_DOUBLE_EQ(five.probability("0110"), 1.); - EXPECT_DOUBLE_EQ(six.probability("0111"), 1.); - EXPECT_DOUBLE_EQ(seven.probability("1000"), 1.); - EXPECT_DOUBLE_EQ(seven.probability("0111"), 0.); - EXPECT_THROW(static_cast(seven.probability("000")), - std::invalid_argument); - EXPECT_THROW(static_cast(seven.probability("000x")), - std::invalid_argument); -} - -TEST(QFTAdderClassical, EvaluatesTheDeterministicResult) { - const QFTAdderClassical benchmark{{.addend = "101"}}; - const auto evaluation = benchmark.evaluate({{"0110", 80}, {"0101", 20}}); - EXPECT_DOUBLE_EQ(evaluation.totalVariationDistance, 0.2); - EXPECT_DOUBLE_EQ(evaluation.squaredHellingerFidelity, 0.8); - ASSERT_TRUE(evaluation.successProbability); - EXPECT_DOUBLE_EQ(*evaluation.successProbability, 0.8); -} - -} // namespace diff --git a/test/bench/test_qft_adder_quantum.cpp b/test/bench/test_qft_adder_quantum.cpp deleted file mode 100644 index 3affe292f0..0000000000 --- a/test/bench/test_qft_adder_quantum.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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/Evaluation.hpp" -#include "bench/QFTAdderQuantum.hpp" - -#include - -#include -#include - -namespace { - -using mqt::bench::Output; -using mqt::bench::QFTAdderQuantum; -using mqt::bench::QFTAdderQuantumOptions; - -TEST(QFTAdderQuantum, StoresTheRegisterWidthAndOutput) { - const QFTAdderQuantum benchmark{{.qubits = 7}}; - EXPECT_EQ(benchmark.options().qubits, 7); - EXPECT_EQ(benchmark.output(), (Output{"result", 14})); -} - -TEST(QFTAdderQuantum, ValidatesTheConfiguredInstance) { - EXPECT_THROW(static_cast(QFTAdderQuantum{{.qubits = 0}}), - std::invalid_argument); - EXPECT_NO_THROW(static_cast( - QFTAdderQuantum{{.qubits = QFTAdderQuantumOptions::MAX_QUBITS}})); - EXPECT_THROW(static_cast(QFTAdderQuantum{ - {.qubits = QFTAdderQuantumOptions::MAX_QUBITS + 1}}), - std::invalid_argument); -} - -TEST(QFTAdderQuantum, GivesUniformWeightToCorrelatedSums) { - const QFTAdderQuantum benchmark{{.qubits = 3}}; - for (const auto* outcome : { - "000001", - "001010", - "010011", - "011100", - "100101", - "101110", - "110111", - "111000", - }) { - EXPECT_DOUBLE_EQ(benchmark.probability(outcome), 1. / 8.); - } - - EXPECT_DOUBLE_EQ(benchmark.probability("000000"), 0.); - EXPECT_DOUBLE_EQ(benchmark.probability("111111"), 0.); - EXPECT_THROW(static_cast(benchmark.probability("00001")), - std::invalid_argument); - EXPECT_THROW(static_cast(benchmark.probability("00000x")), - std::invalid_argument); -} - -TEST(QFTAdderQuantum, EvaluatesTheReferenceWithoutASuccessOutcome) { - const QFTAdderQuantum benchmark{{.qubits = 2}}; - const auto exact = - benchmark.evaluate({{"0001", 1}, {"0110", 1}, {"1011", 1}, {"1100", 1}}); - EXPECT_DOUBLE_EQ(exact.totalVariationDistance, 0.); - EXPECT_DOUBLE_EQ(exact.squaredHellingerFidelity, 1.); - EXPECT_FALSE(exact.successProbability); - - const auto biased = benchmark.evaluate({{"0001", 4}}); - EXPECT_DOUBLE_EQ(biased.totalVariationDistance, 0.75); - EXPECT_DOUBLE_EQ(biased.squaredHellingerFidelity, 0.25); - EXPECT_FALSE(biased.successProbability); -} - -TEST(QFTAdderQuantum, KeepsTheLargestReferenceWeightRepresentable) { - const QFTAdderQuantum benchmark{ - {.qubits = QFTAdderQuantumOptions::MAX_QUBITS}}; - const auto outcome = std::string(QFTAdderQuantumOptions::MAX_QUBITS, '1') + - std::string(QFTAdderQuantumOptions::MAX_QUBITS, '0'); - EXPECT_GT(benchmark.probability(outcome), 0.); -} - -} // namespace diff --git a/test/python/test_bench.py b/test/python/test_bench.py index bda95c7da3..2b71bbbe46 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -22,8 +22,7 @@ grover, multiplexer, qft, - qft_adder_classical, - qft_adder_quantum, + qft_adder, qpe, teleportation, ) @@ -36,8 +35,7 @@ def assert_generates( | grover.Grover | multiplexer.Multiplexer | qft.QFT - | qft_adder_classical.QFTAdderClassical - | qft_adder_quantum.QFTAdderQuantum + | qft_adder.QFTAdder | qpe.QPE | teleportation.Teleportation ), @@ -151,71 +149,59 @@ def test_qft_methods_share_the_periodic_reference() -> None: assert_generates(benchmark) -def test_quantum_qft_adder_reference_json_and_generation() -> None: - """Expose the correlated addend and sum distribution.""" - benchmark = qft_adder_quantum.QFTAdderQuantum(qft_adder_quantum.Options(qubits=2)) - assert benchmark.output.name == "result" - assert benchmark.output.width == 4 - assert benchmark.probability("0001") == pytest.approx(0.25) - assert benchmark.probability("0110") == pytest.approx(0.25) - assert benchmark.probability("1011") == pytest.approx(0.25) - assert benchmark.probability("1100") == pytest.approx(0.25) - assert benchmark.probability("0000") == 0 - - evaluation = benchmark.evaluate({"0001": 1, "0110": 1, "1011": 1, "1100": 1}) - assert evaluation.total_variation_distance == pytest.approx(0) - assert evaluation.squared_hellinger_fidelity == pytest.approx(1) - assert evaluation.success_probability is None - assert json.loads(benchmark.instance_specification_json)["parameters"] == {"qubits": 2} - - instance_copy = qft_adder_quantum.QFTAdderQuantum.from_instance_specification_json( - benchmark.instance_specification_json - ) - manifest_copy = qft_adder_quantum.QFTAdderQuantum.from_manifest_json(benchmark.manifest_json) - assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id - - sampled = qft_adder_quantum.QFTAdderQuantum(qft_adder_quantum.Options(qubits=3)) - shots = 16_384 - counts = mlir.sample(sampled.generate(), shots=shots, seed=17) - assert sum(counts.values()) == shots - assert sampled.evaluate(counts).total_variation_distance < 0.03 +@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 mlir.sample(benchmark.generate(), shots=128, seed=17) == {expected: 128} assert_generates(benchmark) -def test_classical_qft_adder_reference_json_and_generation() -> None: - """Expose exact classical addition without truncating a carry.""" - benchmark = qft_adder_classical.QFTAdderClassical(qft_adder_classical.Options(addend="110")) - assert benchmark.options.addend == "110" - assert benchmark.output.name == "result" - assert benchmark.output.width == 4 - assert benchmark.expected_result == "0111" - assert benchmark.probability("0111") == 1 - assert benchmark.probability("0110") == 0 - - evaluation = benchmark.evaluate({"0111": 8, "0110": 2}) - assert evaluation.total_variation_distance == pytest.approx(0.2) - assert evaluation.squared_hellinger_fidelity == pytest.approx(0.8) - assert evaluation.success_probability == pytest.approx(0.8) - assert json.loads(benchmark.instance_specification_json)["parameters"] == {"addend": "110"} - - instance_copy = qft_adder_classical.QFTAdderClassical.from_instance_specification_json( - benchmark.instance_specification_json - ) - manifest_copy = qft_adder_classical.QFTAdderClassical.from_manifest_json(benchmark.manifest_json) - assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id - - assert_generates(benchmark) +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 + counts = mlir.sample(benchmark.generate(), shots=16_384, seed=17) + assert benchmark.evaluate(counts).total_variation_distance < 0.03 @pytest.mark.parametrize( ("addend", "expected"), [("0", "01"), ("1", "10"), ("001", "0010"), ("110", "0111"), ("111", "1000")], ) -def test_classical_qft_adder_dd_sampling_preserves_width_and_carry(addend: str, expected: str) -> None: - """Execute zero, leading-zero, and carry cases against their exact sums.""" - benchmark = qft_adder_classical.QFTAdderClassical(qft_adder_classical.Options(addend=addend)) - shots = 1_024 - assert mlir.sample(benchmark.generate(), shots=shots, seed=17) == {expected: shots} +def test_qft_adder_preserves_leading_zeros_and_carry(addend: str, expected: str) -> None: + """Keep the input width and final carry in constant addition.""" + benchmark = qft_adder.QFTAdder( + qft_adder.Options( + addend=addend, + accumulator="0" * (len(addend) - 1) + "1", + method=qft_adder.Method.CONSTANT, + overflow=qft_adder.Overflow.CARRY, + ) + ) + assert mlir.sample(benchmark.generate(), shots=128, seed=17) == {expected: 128} def test_qpe_accepts_fraction_and_native_phase() -> None: diff --git a/test/python/test_cli.py b/test/python/test_cli.py index fc895d1863..d375fcffd5 100644 --- a/test/python/test_cli.py +++ b/test/python/test_cli.py @@ -123,8 +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-classical"' in ret.stdout - assert '"qft-adder-quantum"' in ret.stdout + assert '"qft-adder"' in ret.stdout assert '"qpe"' in ret.stdout assert '"teleportation"' in ret.stdout From a5709c92fa2e22c296823574f5586f6b959c077a Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 14:12:01 +0000 Subject: [PATCH 21/24] =?UTF-8?q?=F0=9F=A7=AA=20Check=20small=20QFT=20addi?= =?UTF-8?q?tions=20and=20relative=20phases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exhaust all width-one-through-three basis operands for both methods and overflow policies. Check coherent register-addition statevectors against independent amplitudes up to global phase. Assisted-by: GPT-6 via Codex --- .agent/plans/qft-adder.md | 21 +++++++-------------- test/python/test_bench.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/.agent/plans/qft-adder.md b/.agent/plans/qft-adder.md index fc7903ef47..117e5abbc5 100644 --- a/.agent/plans/qft-adder.md +++ b/.agent/plans/qft-adder.md @@ -1,6 +1,6 @@ # QFT adder benchmark -Status: in progress; validate the unified API and exhaustive small-width tests. +Status: complete. ## Goal and scope @@ -26,20 +26,13 @@ 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. -## Work remaining - -- [ ] Validate options, references, JSON round trips, bindings, and QC/jeff - generation. -- [ ] Check every operand pair at widths one through three for both methods and - overflow policies. -- [ ] Check coherent addition amplitudes, then run required lint and stub - generation. - ## Validation Run the native benchmark and generation binaries and `uv run --no-sync pytest test/python/test_bench.py test/python/test_cli.py -k bench`. -Local implementation checks passed: 47 native reference/JSON tests, 17 -generation tests, and 26 Python benchmark/CLI tests. Stubs were regenerated. The -implementation belongs to #2404; #2408 adds exhaustive arithmetic and -phase-sensitive execution checks. Both retain the existing PR chain. +Local checks passed: 47 native reference/JSON tests, 17 generation tests, and 44 +Python benchmark/CLI tests. The latter include all 336 operand pairs across +widths one through three, both methods, and both overflow policies, plus 28 +coherent register-addition statevectors. Stubs were regenerated. The +implementation is in #2404; #2408 adds the exhaustive and phase-sensitive +execution tests. Validation is bounded to these widths and supported inputs. diff --git a/test/python/test_bench.py b/test/python/test_bench.py index 2b71bbbe46..24947842bf 100644 --- a/test/python/test_bench.py +++ b/test/python/test_bench.py @@ -12,7 +12,9 @@ import json from fractions import Fraction +from itertools import product +import numpy as np import pytest from mqt.core import bench, mlir @@ -204,6 +206,43 @@ def test_qft_adder_preserves_leading_zeros_and_carry(addend: str, expected: str) assert mlir.sample(benchmark.generate(), shots=128, seed=17) == {expected: 128} +@pytest.mark.parametrize("width", [1, 2, 3]) +@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_all_small_operands(width: int, method: qft_adder.Method, overflow: qft_adder.Overflow) -> None: + """Compare every small operand pair with independent integer addition.""" + sum_width = width + (overflow == qft_adder.Overflow.CARRY) + for addend, accumulator in product(range(1 << width), repeat=2): + addend_bits = f"{addend:0{width}b}" + benchmark = qft_adder.QFTAdder( + qft_adder.Options( + addend=addend_bits, accumulator=f"{accumulator:0{width}b}", method=method, overflow=overflow + ) + ) + total = (addend + accumulator) % (1 << sum_width) + expected = (addend_bits if method == qft_adder.Method.REGISTER else "") + f"{total:0{sum_width}b}" + assert benchmark.expected_result == expected + assert mlir.sample(benchmark.generate(), shots=32, seed=17) == {expected: 32}, (addend, accumulator) + + +@pytest.mark.parametrize("width", [1, 2, 3]) +@pytest.mark.parametrize("overflow", [qft_adder.Overflow.WRAP, qft_adder.Overflow.CARRY]) +def test_qft_adder_preserves_relative_phases(width: int, overflow: qft_adder.Overflow) -> None: + """Check coherent register addition for every small basis accumulator.""" + sum_width = width + (overflow == qft_adder.Overflow.CARRY) + for accumulator in range(1 << width): + benchmark = qft_adder.QFTAdder( + qft_adder.Options(addend="+" * width, accumulator=f"{accumulator:0{width}b}", overflow=overflow) + ) + actual = mlir.simulate(benchmark.generate()) + expected = np.zeros(1 << (width + sum_width), dtype=np.complex128) + for addend in range(1 << width): + total = (addend + accumulator) % (1 << sum_width) + expected[(total << width) | addend] = 2 ** (-width / 2) + phase = np.exp(1j * np.angle(np.vdot(expected, actual))) + np.testing.assert_allclose(actual, phase * expected, rtol=0, atol=1e-12) + + def test_qpe_accepts_fraction_and_native_phase() -> None: """Use exact rational input without a free-form parameter dictionary.""" options = qpe.Options( From 03214fb16b87afc9646d24a6f27de818f3fa962f Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 15:07:32 +0000 Subject: [PATCH 22/24] =?UTF-8?q?=F0=9F=93=9D=20Fold=20adder=20validation?= =?UTF-8?q?=20into=20one=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-6 via Codex --- .agent/plans/qft-adder.md | 4 ++-- CHANGELOG.md | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.agent/plans/qft-adder.md b/.agent/plans/qft-adder.md index 117e5abbc5..76fd332b12 100644 --- a/.agent/plans/qft-adder.md +++ b/.agent/plans/qft-adder.md @@ -34,5 +34,5 @@ Local checks passed: 47 native reference/JSON tests, 17 generation tests, and 44 Python benchmark/CLI tests. The latter include all 336 operand pairs across widths one through three, both methods, and both overflow policies, plus 28 coherent register-addition statevectors. Stubs were regenerated. The -implementation is in #2404; #2408 adds the exhaustive and phase-sensitive -execution tests. Validation is bounded to these widths and supported inputs. +implementation and its exhaustive, phase-sensitive execution tests are in #2404. +Validation is bounded to these widths and supported inputs. diff --git a/CHANGELOG.md b/CHANGELOG.md index ecee6ceb6e..3b9aca651f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,8 +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], [#2404], [#2408]) ([**@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 @@ -928,7 +927,6 @@ for previous changelogs._ [#2421]: https://github.com/munich-quantum-toolkit/core/pull/2421 -[#2408]: https://github.com/munich-quantum-toolkit/core/pull/2408 [#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 From c00429d4f603d61e5990ea4c000471a971e854b3 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 7 Sep 2026 15:26:03 +0000 Subject: [PATCH 23/24] =?UTF-8?q?=F0=9F=A7=AA=20Test=20QFT=20adder=20arith?= =?UTF-8?q?metic=20and=20coherence=20in=20C++?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve all 336 basis-input and 28 coherent-input cases using the shared native compiler path and DD simulator. Keep per-family Python tests focused on bindings. Assisted-by: GPT-6 via Codex --- .agent/plans/qft-adder.md | 15 +- mlir/unittests/bench/CMakeLists.txt | 2 +- .../test_benchmark_generate_qft_adder.cpp | 95 +++++ test/python/bench/test_qft_adder.py | 54 +++ test/python/test_bench.py | 328 ------------------ 5 files changed, 158 insertions(+), 336 deletions(-) create mode 100644 test/python/bench/test_qft_adder.py delete mode 100644 test/python/test_bench.py diff --git a/.agent/plans/qft-adder.md b/.agent/plans/qft-adder.md index 76fd332b12..8d2a58fd7e 100644 --- a/.agent/plans/qft-adder.md +++ b/.agent/plans/qft-adder.md @@ -29,10 +29,11 @@ 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/test_bench.py test/python/test_cli.py -k bench`. -Local checks passed: 47 native reference/JSON tests, 17 generation tests, and 44 -Python benchmark/CLI tests. The latter include all 336 operand pairs across -widths one through three, both methods, and both overflow policies, plus 28 -coherent register-addition statevectors. Stubs were regenerated. The -implementation and its exhaustive, phase-sensitive execution tests are in #2404. -Validation is bounded to these widths and supported inputs. +`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/mlir/unittests/bench/CMakeLists.txt b/mlir/unittests/bench/CMakeLists.txt index 2462ed92bc..379c1c7978 100644 --- a/mlir/unittests/bench/CMakeLists.txt +++ b/mlir/unittests/bench/CMakeLists.txt @@ -21,7 +21,7 @@ add_executable( 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/test_benchmark_generate_qft_adder.cpp b/mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp index 853881a6a4..479bee586f 100644 --- a/mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp +++ b/mlir/unittests/bench/test_benchmark_generate_qft_adder.cpp @@ -9,7 +9,11 @@ */ #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" @@ -26,8 +30,12 @@ #include #include +#include +#include #include +#include #include +#include #include #include @@ -212,4 +220,91 @@ TEST(GenerateProgramTest, KeepsLargestClassicalQFTAdderFiniteAndStructured) { 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/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_bench.py b/test/python/test_bench.py deleted file mode 100644 index 24947842bf..0000000000 --- a/test/python/test_bench.py +++ /dev/null @@ -1,328 +0,0 @@ -# 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 - -"""Tests for typed benchmark instances and analytic references.""" - -from __future__ import annotations - -import json -from fractions import Fraction -from itertools import product - -import numpy as np -import pytest - -from mqt.core import bench, mlir -from mqt.core.bench import ( - bv, - ghz, - grover, - multiplexer, - qft, - qft_adder, - qpe, - teleportation, -) - - -def assert_generates( - benchmark: ( - bv.BV - | ghz.GHZ - | grover.Grover - | multiplexer.Multiplexer - | qft.QFT - | qft_adder.QFTAdder - | qpe.QPE - | teleportation.Teleportation - ), -) -> None: - """Exercise the shared Python-to-MLIR generation boundary.""" - program = benchmark.generate() - assert isinstance(program, mlir.QCProgram) - assert "qc." in program.ir - assert isinstance(program.to_qco(), mlir.QCOProgram) - - -def test_bv_methods_share_the_hidden_string_reference() -> None: - """Expose static and dynamic Bernstein--Vazirani as one family.""" - for method in (bv.Method.STATIC, bv.Method.DYNAMIC): - benchmark = bv.BV(bv.Options(hidden_bitstring="101", method=method)) - assert benchmark.probability("101") == 1 - assert benchmark.evaluate({"101": 10}).success_probability == 1 - assert bv.BV.from_manifest_json(benchmark.manifest_json).case_id == benchmark.case_id - assert_generates(benchmark) - - -def test_ghz_options_reference_and_json_roundtrip() -> None: - """Keep GHZ parameters typed and preserve one semantic case through JSON.""" - with pytest.raises(TypeError): - ghz.Options(3) # ty: ignore[missing-argument, too-many-positional-arguments] - - options = ghz.Options( - qubits=3, - topology=ghz.Topology.STAR, - basis=ghz.Basis.X, - ) - with pytest.raises(AttributeError): - options.qubits = 4 # ty: ignore[invalid-assignment] - - benchmark = ghz.GHZ(options) - assert isinstance(benchmark.output, bench.Output) - assert benchmark.output.name == "result" - assert benchmark.output.width == 3 - assert benchmark.probability("011") == pytest.approx(0.25) - assert benchmark.probability("111") == 0 - - evaluation = benchmark.evaluate({"000": 50, "011": 50}) - assert isinstance(evaluation, bench.Evaluation) - assert evaluation.total_variation_distance == pytest.approx(0.5) - assert evaluation.squared_hellinger_fidelity == pytest.approx(0.5) - assert evaluation.success_probability is None - - instance_copy = ghz.GHZ.from_instance_specification_json(benchmark.instance_specification_json) - manifest_copy = ghz.GHZ.from_manifest_json(benchmark.manifest_json) - assert instance_copy.instance_specification_json == benchmark.instance_specification_json - assert manifest_copy.manifest_json == benchmark.manifest_json - assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id - assert_generates(benchmark) - - -def test_grover_resolves_iterations_and_reports_success() -> None: - """Expose Grover's resolved default and marked-outcome score.""" - options = grover.Options(marked_bitstring="10") - benchmark = grover.Grover(options) - - assert options.iterations is None - assert benchmark.options.iterations == 1 - assert benchmark.qubits == 2 - assert benchmark.probability("10") == pytest.approx(1) - assert benchmark.evaluate({"10": 20}).success_probability == pytest.approx(1) - - copy = grover.Grover.from_manifest_json(benchmark.manifest_json) - assert copy.instance_specification_json == benchmark.instance_specification_json - assert copy.case_id == benchmark.case_id - assert_generates(benchmark) - - -def test_multiplexer_reference_json_and_generation() -> None: - """Expose the fixed-angle quantum multiplexer as one typed family.""" - benchmark = multiplexer.Multiplexer(multiplexer.Options(qubits=3)) - assert benchmark.output.name == "result" - assert benchmark.output.width == 3 - assert benchmark.probability("000") == pytest.approx(0.25) - assert benchmark.probability("001") == 0 - - evaluation = benchmark.evaluate({"000": 10}) - assert evaluation.total_variation_distance == pytest.approx(0.75) - assert evaluation.squared_hellinger_fidelity == pytest.approx(0.25) - assert evaluation.success_probability is None - assert json.loads(benchmark.instance_specification_json)["parameters"] == {"qubits": 3} - - instance_copy = multiplexer.Multiplexer.from_instance_specification_json(benchmark.instance_specification_json) - manifest_copy = multiplexer.Multiplexer.from_manifest_json(benchmark.manifest_json) - assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id - - shots = 16_384 - counts = mlir.sample(benchmark.generate(), shots=shots, seed=17) - assert sum(counts.values()) == shots - assert benchmark.evaluate(counts).total_variation_distance < 0.03 - assert_generates(benchmark) - - -def test_qft_methods_share_the_periodic_reference() -> None: - """Expose standard and semiclassical QFT as one family.""" - for method in (qft.Method.STANDARD, qft.Method.SEMICLASSICAL): - benchmark = qft.QFT(qft.Options(qubits=3, period_exponent=1, method=method)) - assert benchmark.probability("000") == pytest.approx(0.5) - assert benchmark.probability("100") == pytest.approx(0.5) - assert ( - qft.QFT.from_instance_specification_json(benchmark.instance_specification_json).case_id == benchmark.case_id - ) - shots = 16_384 - counts = mlir.sample(benchmark.generate(), shots=shots, seed=17) - assert sum(counts.values()) == shots - assert benchmark.evaluate(counts).total_variation_distance < 0.03 - assert_generates(benchmark) - - -@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 mlir.sample(benchmark.generate(), shots=128, seed=17) == {expected: 128} - assert_generates(benchmark) - - -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 - counts = mlir.sample(benchmark.generate(), shots=16_384, seed=17) - assert benchmark.evaluate(counts).total_variation_distance < 0.03 - - -@pytest.mark.parametrize( - ("addend", "expected"), - [("0", "01"), ("1", "10"), ("001", "0010"), ("110", "0111"), ("111", "1000")], -) -def test_qft_adder_preserves_leading_zeros_and_carry(addend: str, expected: str) -> None: - """Keep the input width and final carry in constant addition.""" - benchmark = qft_adder.QFTAdder( - qft_adder.Options( - addend=addend, - accumulator="0" * (len(addend) - 1) + "1", - method=qft_adder.Method.CONSTANT, - overflow=qft_adder.Overflow.CARRY, - ) - ) - assert mlir.sample(benchmark.generate(), shots=128, seed=17) == {expected: 128} - - -@pytest.mark.parametrize("width", [1, 2, 3]) -@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_all_small_operands(width: int, method: qft_adder.Method, overflow: qft_adder.Overflow) -> None: - """Compare every small operand pair with independent integer addition.""" - sum_width = width + (overflow == qft_adder.Overflow.CARRY) - for addend, accumulator in product(range(1 << width), repeat=2): - addend_bits = f"{addend:0{width}b}" - benchmark = qft_adder.QFTAdder( - qft_adder.Options( - addend=addend_bits, accumulator=f"{accumulator:0{width}b}", method=method, overflow=overflow - ) - ) - total = (addend + accumulator) % (1 << sum_width) - expected = (addend_bits if method == qft_adder.Method.REGISTER else "") + f"{total:0{sum_width}b}" - assert benchmark.expected_result == expected - assert mlir.sample(benchmark.generate(), shots=32, seed=17) == {expected: 32}, (addend, accumulator) - - -@pytest.mark.parametrize("width", [1, 2, 3]) -@pytest.mark.parametrize("overflow", [qft_adder.Overflow.WRAP, qft_adder.Overflow.CARRY]) -def test_qft_adder_preserves_relative_phases(width: int, overflow: qft_adder.Overflow) -> None: - """Check coherent register addition for every small basis accumulator.""" - sum_width = width + (overflow == qft_adder.Overflow.CARRY) - for accumulator in range(1 << width): - benchmark = qft_adder.QFTAdder( - qft_adder.Options(addend="+" * width, accumulator=f"{accumulator:0{width}b}", overflow=overflow) - ) - actual = mlir.simulate(benchmark.generate()) - expected = np.zeros(1 << (width + sum_width), dtype=np.complex128) - for addend in range(1 << width): - total = (addend + accumulator) % (1 << sum_width) - expected[(total << width) | addend] = 2 ** (-width / 2) - phase = np.exp(1j * np.angle(np.vdot(expected, actual))) - np.testing.assert_allclose(actual, phase * expected, rtol=0, atol=1e-12) - - -def test_qpe_accepts_fraction_and_native_phase() -> None: - """Use exact rational input without a free-form parameter dictionary.""" - options = qpe.Options( - precision=2, - phase=Fraction(3, 24), - method=qpe.Method.ITERATIVE, - ) - assert options.phase == Fraction(1, 8) - - benchmark = qpe.QPE(options) - assert benchmark.probability("00") == pytest.approx((2 + 2**0.5) / 8) - assert benchmark.probability("01") == pytest.approx((2 + 2**0.5) / 8) - assert json.loads(benchmark.instance_specification_json)["parameters"]["phase"] == { - "denominator": 8, - "numerator": 1, - } - - instance_copy = qpe.QPE.from_instance_specification_json(benchmark.instance_specification_json) - assert instance_copy.options.phase == Fraction(1, 8) - assert instance_copy.options.method is qpe.Method.ITERATIVE - assert instance_copy.case_id == benchmark.case_id - - phase = qpe.Phase(numerator=9, denominator=8) - native_options = qpe.Options(precision=3, phase=phase) - assert phase.numerator == 1 - assert phase.denominator == 8 - assert native_options.phase == Fraction(1, 8) - assert_generates(benchmark) - - -@pytest.mark.parametrize("method", [qpe.Method.STANDARD, qpe.Method.ITERATIVE]) -@pytest.mark.parametrize("phase", [Fraction(3, 8), Fraction(1, 3)]) -def test_qpe_dd_sampling_matches_reference(method: qpe.Method, phase: Fraction) -> None: - """Execute exact and inexact phases with both inverse-QFT implementations.""" - benchmark = qpe.QPE(qpe.Options(precision=3, phase=phase, method=method)) - shots = 16_384 - counts = mlir.sample(benchmark.generate(), shots=shots, seed=17) - assert sum(counts.values()) == shots - assert benchmark.evaluate(counts).total_variation_distance < 0.03 - - -def test_qpe_rejects_untyped_phase_input() -> None: - """Reject generic dictionaries at the typed Python boundary.""" - with pytest.raises(TypeError, match=r"fractions\.Fraction or Phase"): - qpe.Options( - precision=3, - phase={"numerator": 1, "denominator": 8}, # ty: ignore[invalid-argument-type] - ) - - -def test_qpe_normalizes_arbitrary_fraction() -> None: - """Normalize arbitrary-size fractions before entering the native type.""" - negative = qpe.Options(precision=3, phase=Fraction(-1, 8)) - large = qpe.Options(precision=3, phase=Fraction(2**80 + 1, 8)) - assert negative.phase == Fraction(7, 8) - assert large.phase == Fraction(1, 8) - - with pytest.raises(ValueError, match="denominator must fit in 64 bits"): - qpe.Options(precision=3, phase=Fraction(1, 2**80 + 1)) - - -def test_teleportation_reference_json_and_generation() -> None: - """Expose the fixed quantum teleportation benchmark without options.""" - benchmark = teleportation.Teleportation() - assert benchmark.output.name == "result" - assert benchmark.output.width == 1 - assert benchmark.probability("0") == 1 - assert benchmark.probability("1") == 0 - - evaluation = benchmark.evaluate({"0": 128}) - assert evaluation.total_variation_distance == pytest.approx(0) - assert evaluation.squared_hellinger_fidelity == pytest.approx(1) - assert evaluation.success_probability == 1 - assert json.loads(benchmark.instance_specification_json)["parameters"] == {} - - instance_copy = teleportation.Teleportation.from_instance_specification_json(benchmark.instance_specification_json) - manifest_copy = teleportation.Teleportation.from_manifest_json(benchmark.manifest_json) - assert instance_copy.case_id == manifest_copy.case_id == benchmark.case_id - - shots = 128 - counts = mlir.sample(benchmark.generate(), shots=shots, seed=17) - assert counts == {"0": shots} - assert_generates(benchmark) From 3339f927f391562e0013c82c6dd28f62d16a428d Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:59:57 +0200 Subject: [PATCH 24/24] Refine QFT adder documentation Assisted-by: GPT-5.6 Sol via Codex --- bindings/bench/register_qft_adder.cpp | 2 +- docs/benchmarks.md | 4 +++- python/mqt/core/bench/qft_adder.pyi | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bindings/bench/register_qft_adder.cpp b/bindings/bench/register_qft_adder.cpp index 72e17797b2..f42754b7db 100644 --- a/bindings/bench/register_qft_adder.cpp +++ b/bindings/bench/register_qft_adder.cpp @@ -43,7 +43,7 @@ void registerQFTAdder(const nb::module_& m) { "overflow"_a = bench::QFTAdderOverflow::Wrap) .def_ro( "addend", &bench::QFTAdderOptions::addend, - "Big-endian addend; register inputs also accept '+' for a |+> qubit.") + 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, diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 144b5c3806..bdcf42b6ac 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -237,7 +237,9 @@ resolved parameters, logical output, reference, definition version, and case ID. Before evaluation, normalize backend results to the manifest's big-endian `result` order. -## QFT addition +## 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 diff --git a/python/mqt/core/bench/qft_adder.pyi b/python/mqt/core/bench/qft_adder.pyi index b5411f408b..63178b95e6 100644 --- a/python/mqt/core/bench/qft_adder.pyi +++ b/python/mqt/core/bench/qft_adder.pyi @@ -36,7 +36,7 @@ class Options: ) -> None: ... @property def addend(self) -> str: - """Big-endian addend; register inputs also accept '+' for a |+> qubit.""" + """Big-endian addend; register inputs also accept ``+`` for a :math:`|+\\rangle` qubit.""" @property def accumulator(self) -> str: