From 21d1baa85db36f8f30145ca654a9d66af6b9c21e Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sat, 12 Sep 2026 09:48:29 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20W-state=20preparation=20bench?= =?UTF-8?q?mark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare W states in structured loops and evaluate sampled counts against analytic single-excitation probabilities. Register the family in the existing C++, Python, and JSON interfaces. Retain the 4,096-qubit DD regression and the control-flow budget needed for large structured preparations. Assisted-by: Codex --- .agent/plans/w-state-benchmark.md | 53 +++++++++ bindings/bench/CMakeLists.txt | 1 + bindings/bench/register_bench.cpp | 4 + bindings/bench/register_w_state.cpp | 80 ++++++++++++++ docs/benchmarks.md | 39 +++++++ docs/glossary.md | 4 + include/mqt-core/bench/BenchmarkFamilies.inc | 2 + include/mqt-core/bench/JSON.hpp | 3 +- include/mqt-core/bench/WState.hpp | 42 ++++++++ mlir/bench/programs/CMakeLists.txt | 3 +- mlir/bench/programs/Programs.h | 5 + mlir/bench/programs/WState.cpp | 62 +++++++++++ .../mqt/Dialect/QCO/Utils/DDFunctionality.h | 2 +- mlir/include/mqt/bench/Generate.h | 4 + .../lib/Dialect/QCO/Utils/DDFunctionality.cpp | 11 +- .../QCO/Utils/test_dd_functionality.cpp | 25 ++--- mlir/unittests/bench/CMakeLists.txt | 3 +- mlir/unittests/bench/test_benchmark_cli.cmake | 35 +++++- .../bench/test_benchmark_generate_w_state.cpp | 102 ++++++++++++++++++ python/mqt/core/bench/__init__.pyi | 1 + python/mqt/core/bench/w_state.pyi | 63 +++++++++++ src/bench/JSON.cpp | 42 ++++++++ src/bench/WState.cpp | 49 +++++++++ test/bench/test_json.cpp | 2 +- test/bench/test_w_state.cpp | 88 +++++++++++++++ test/python/bench/test_w_state.py | 54 ++++++++++ 26 files changed, 757 insertions(+), 22 deletions(-) create mode 100644 .agent/plans/w-state-benchmark.md create mode 100644 bindings/bench/register_w_state.cpp create mode 100644 include/mqt-core/bench/WState.hpp create mode 100644 mlir/bench/programs/WState.cpp create mode 100644 mlir/unittests/bench/test_benchmark_generate_w_state.cpp create mode 100644 python/mqt/core/bench/w_state.pyi create mode 100644 src/bench/WState.cpp create mode 100644 test/bench/test_w_state.cpp create mode 100644 test/python/bench/test_w_state.py diff --git a/.agent/plans/w-state-benchmark.md b/.agent/plans/w-state-benchmark.md new file mode 100644 index 0000000000..a78fe326b6 --- /dev/null +++ b/.agent/plans/w-state-benchmark.md @@ -0,0 +1,53 @@ +# W-state preparation + +Status: complete; stacked on PR #2545 and validated locally. + +## Outcome and scope + +The `w-state` family provides C++ `WState`, Python `mqt.core.bench.w_state`, and +the existing benchmark CLI interfaces. It prepares the equal, positive-amplitude +superposition of single-excitation states, returning every Z measurement in +`result`. Qubit zero is the least significant displayed bit. The required +positive `qubits` value must fit circuit indices and angle storage. + +Generation in `mlir/bench/programs/WState.cpp` initializes qubit zero, then uses +an `scf.for` sweep of controlled RY and reverse CX. A rank-one tensor holds +precomputed angles, so jeff needs no runtime transcendental operations. + +## Decisions and ownership + +`src/bench/WState.cpp` provides the analytic probability: `1/n` for a +single-excitation bitstring, zero otherwise. It reuses the existing counts +metrics, JSON registry, and binding patterns. No state container, sparse input +API, or new simulator entry point is needed for this distribution evaluation. +Existing family interfaces, manifests, and case IDs remain unchanged. + +Generation tests use existing QCO DD simulation and `dd::makeWState` to check +amplitudes and coherence. These implementation checks do not expand the public +benchmark API. The 4,096-qubit test checks structured generation, jeff +serialization and reload, and simulation without dense extraction. Reference +size alone does not establish a simulation-time bound for arbitrary circuits. + +The QCO DD interpreter shares a 100-million-step budget across loops, branches, +and calls. It rejects resolved `scf.for` trip counts above the remaining budget +before execution; per-step accounting remains for nested flow and while loops. +Budget tests use oversized inner loops to fail promptly and retain a successful +case above the previous 10,000-step boundary. No public configuration was added. + +## Validation + +Run the release preset's `mqt-core-bench-test`, +`mqt-core-mlir-unittests-benchmark`, and `mqt-core-mlir-unittest-qco-utils` +binaries, plus the `mqt-core-mlir-benchmark-cli` CTest. Run the Python +benchmark, MLIR, QCO DD, and loop suites. + +Regenerate stubs, build the executable documentation, and run full-file C++ lint +and repository lint as required by [AGENTS.md](../../AGENTS.md). Compare C++ +lint against `origin/codex/fix-mlir-exception-boundaries`, the PR's base branch. +The docs include checked 3- and 256-qubit sampling examples; the native +regression retains the 4,096-qubit check. + +Local validation passed: 66 benchmark tests, 33 generation tests, 196 QCO +utility tests, the CLI CTest, and 199 focused Python tests. Stub generation, +full-file C++ lint, repository lint, and executable documentation passed. Hosted +checks are reported separately in the PR. diff --git a/bindings/bench/CMakeLists.txt b/bindings/bench/CMakeLists.txt index e3a38e5907..b440d4001d 100644 --- a/bindings/bench/CMakeLists.txt +++ b/bindings/bench/CMakeLists.txt @@ -12,6 +12,7 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}-bench-bindings) register_bv.cpp register_modular_multiplier.cpp register_ghz.cpp + register_w_state.cpp register_grover.cpp register_multiplexer.cpp register_qft.cpp diff --git a/bindings/bench/register_bench.cpp b/bindings/bench/register_bench.cpp index bb7d5ff0a8..2d3d65f6a4 100644 --- a/bindings/bench/register_bench.cpp +++ b/bindings/bench/register_bench.cpp @@ -22,6 +22,7 @@ namespace nb = nanobind; void registerBV(const nb::module_& m); void registerModularMultiplier(const nb::module_& m); void registerGHZ(const nb::module_& m); +void registerWState(const nb::module_& m); void registerGrover(const nb::module_& m); void registerMultiplexer(const nb::module_& m); void registerQFT(const nb::module_& m); @@ -52,6 +53,9 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { .def_ro("success_probability", &bench::Evaluation::successProbability, "The observed success probability, when defined."); + registerWState( + m.def_submodule("w_state", "W-state preparation instances and options.")); + const nb::module_ bv = m.def_submodule( "bv", "Bernstein--Vazirani benchmark instances and options."); registerBV(bv); diff --git a/bindings/bench/register_w_state.cpp b/bindings/bench/register_w_state.cpp new file mode 100644 index 0000000000..ae19a71045 --- /dev/null +++ b/bindings/bench/register_w_state.cpp @@ -0,0 +1,80 @@ +/* + * 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/WState.hpp" + +#include "nanobind/nanobind.h" +#include "nanobind/stl/map.h" /// NOLINT(misc-include-cleaner) +#include "nanobind/stl/string.h" /// NOLINT(misc-include-cleaner) +#include "nanobind/stl/string_view.h" /// NOLINT(misc-include-cleaner) + +#include + +namespace mqt { + +namespace nb = nanobind; +using namespace nb::literals; + +/// NOLINTNEXTLINE(misc-use-internal-linkage) +void registerWState(const nb::module_& m) { + nb::class_(m, "Options", + "Parameters for W-state preparation.") + .def(nb::init(), nb::kw_only(), "qubits"_a) + .def_ro("qubits", &bench::WStateOptions::qubits, "The number of qubits."); + + auto wState = nb::class_( + m, "WState", "A validated W-state preparation benchmark."); + wState.def(nb::init(), "options"_a) + .def_prop_ro("options", &bench::WState::options, + nb::rv_policy::reference_internal, + "The resolved benchmark parameters.") + .def_prop_ro("output", &bench::WState::output, + nb::rv_policy::reference_internal, + "The logical output register.") + .def("probability", &bench::WState::probability, "outcome"_a, + "Return the ideal probability of an outcome.") + .def("evaluate", &bench::WState::evaluate, "counts"_a, + "Compare sampled counts with the ideal distribution.") + .def( + "generate", + [](const bench::WState& 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::WState& value) { + return bench::toInstanceSpecificationJSON(value); + }, + "The canonical instance specification JSON.") + .def_prop_ro( + "manifest_json", + [](const bench::WState& value) { + return bench::toManifestJSON(value); + }, + "The canonical manifest JSON.") + .def_prop_ro( + "case_id", + [](const bench::WState& value) { return bench::caseId(value); }, + "The stable semantic case ID.") + .def_static("from_instance_specification_json", + &bench::wStateFromInstanceSpecificationJSON, "json"_a, + nb::kw_only(), "source"_a = "", + "Parse a strict benchmark instance specification.") + .def_static("from_manifest_json", &bench::wStateFromManifestJSON, + "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 4da258c121..400d9172d1 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -264,6 +264,45 @@ Before evaluation, normalize backend results to the manifest's big-endian ## Benchmark families +### W-state preparation + +The `w-state` family prepares the equal, positive-amplitude superposition of all +single-excitation states: + +```{math} +|W_n\rangle = \frac{1}{\sqrt n}\sum_{j=0}^{n-1}|2^j\rangle. +``` + +The required `qubits` parameter is positive. All qubits are measured in Z; +result bit $i$ is qubit $i$. + +The ideal probability is $1/n$ for each single-excitation bitstring and zero +otherwise. The existing counts evaluator compares observations with this +analytic distribution: + +```{code-cell} ipython3 +from mqt.core import mlir +from mqt.core.bench import w_state + +w = w_state.WState(w_state.Options(qubits=3)) +counts = mlir.sample(w.generate(), shots=4096, seed=17) +assert set(counts) == {"001", "010", "100"} +assert w.evaluate(counts).total_variation_distance < 0.03 +assert w.probability("010") == 1 / 3 +``` + +DD sampling also supports larger instances without dense statevector extraction. +Runtime and memory depend on the intermediate DDs. + +```{code-cell} ipython3 +large_w = w_state.WState(w_state.Options(qubits=256)) +large_program = large_w.generate() +large_counts = mlir.sample(large_program, shots=64, seed=17) +assert large_program.is_valid +assert sum(large_counts.values()) == 64 +assert all(len(outcome) == 256 and outcome.count("1") == 1 for outcome in large_counts) +``` + ### QFT addition The `qft-adder` family adds two equal-width operands. `REGISTER` stores the diff --git a/docs/glossary.md b/docs/glossary.md index 4f9afd723e..e5b7f3e54e 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -321,6 +321,10 @@ semiclassical QFT alias:** semiclassical QFT. A quantum Fourier-transform method that measures, resets, and reuses one qubit for each output bit. Each round applies rotations controlled by earlier measurement results. + +W state + **Preferred term:** W state. The equal, positive-amplitude superposition of + all computational-basis states with exactly one qubit in state one. ``` ## Index diff --git a/include/mqt-core/bench/BenchmarkFamilies.inc b/include/mqt-core/bench/BenchmarkFamilies.inc index 3437c431cc..9d04b9c58b 100644 --- a/include/mqt-core/bench/BenchmarkFamilies.inc +++ b/include/mqt-core/bench/BenchmarkFamilies.inc @@ -37,4 +37,6 @@ MQT_BENCHMARK_FAMILY(RepeatUntilSuccess, repeatUntilSuccess, "repeat-until-success", 1) MQT_BENCHMARK_FAMILY(Teleportation, teleportation, "teleportation", 1) +MQT_BENCHMARK_FAMILY(WState, wState, "w-state", 1) + #undef MQT_BENCHMARK_FAMILY diff --git a/include/mqt-core/bench/JSON.hpp b/include/mqt-core/bench/JSON.hpp index bd5ad763f4..19439e117f 100644 --- a/include/mqt-core/bench/JSON.hpp +++ b/include/mqt-core/bench/JSON.hpp @@ -21,6 +21,7 @@ #include "bench/QPE.hpp" #include "bench/RepeatUntilSuccess.hpp" #include "bench/Teleportation.hpp" +#include "bench/WState.hpp" #include "bench/mqt_core_bench_export.h" #include @@ -33,7 +34,7 @@ namespace mqt::bench { /// One validated benchmark instance from the JSON registry. using BenchmarkInstance = std::variant; + QPE, RepeatUntilSuccess, Teleportation, WState>; /// A diagnostic returned by a fallible JSON operation. struct JSONError { diff --git a/include/mqt-core/bench/WState.hpp b/include/mqt-core/bench/WState.hpp new file mode 100644 index 0000000000..92029f7df6 --- /dev/null +++ b/include/mqt-core/bench/WState.hpp @@ -0,0 +1,42 @@ +/* + * 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 W-state preparation. +struct WStateOptions { + /// Positive number of qubits; circuit dimensions must fit signed 64-bit + /// indices. + size_t qubits; +}; + +/// Prepare the equal, positive-amplitude superposition of single excitations. +class MQT_CORE_BENCH_EXPORT WState final { +public: + explicit WState(WStateOptions options); + [[nodiscard]] const WStateOptions& options() const noexcept; + [[nodiscard]] const Output& output() const noexcept; + [[nodiscard]] double probability(std::string_view outcome) const; + [[nodiscard]] Evaluation evaluate(const Counts& counts) const; + +private: + WStateOptions options_; + Output output_; +}; + +} // namespace mqt::bench diff --git a/mlir/bench/programs/CMakeLists.txt b/mlir/bench/programs/CMakeLists.txt index 83bf2d2919..0fbc5a8869 100644 --- a/mlir/bench/programs/CMakeLists.txt +++ b/mlir/bench/programs/CMakeLists.txt @@ -18,7 +18,8 @@ add_library( QFTUtils.cpp QPE.cpp RepeatUntilSuccess.cpp - Teleportation.cpp) + Teleportation.cpp + WState.cpp) target_link_libraries(MQTBenchmarkPrograms PUBLIC MQT::CoreBench MLIRQCProgramBuilder MLIRArithDialect MLIRTensorDialect) mqt_mlir_target_use_project_options(MQTBenchmarkPrograms) diff --git a/mlir/bench/programs/Programs.h b/mlir/bench/programs/Programs.h index b5e792fdaf..65e907f62a 100644 --- a/mlir/bench/programs/Programs.h +++ b/mlir/bench/programs/Programs.h @@ -21,6 +21,7 @@ namespace mqt::bench { class BV; class ModularMultiplier; class GHZ; +class WState; class Grover; class Multiplexer; class QFT; @@ -41,6 +42,10 @@ SmallVector bv(qc::QCProgramBuilder& builder, const BV& benchmark); SmallVector modularMultiplier(qc::QCProgramBuilder& builder, const ModularMultiplier& benchmark); +/// Emit W-state preparation. +SmallVector wState(qc::QCProgramBuilder& builder, + const WState& benchmark); + /// Emit one configured GHZ benchmark. SmallVector ghz(qc::QCProgramBuilder& builder, const GHZ& benchmark); diff --git a/mlir/bench/programs/WState.cpp b/mlir/bench/programs/WState.cpp new file mode 100644 index 0000000000..da1911f7bf --- /dev/null +++ b/mlir/bench/programs/WState.cpp @@ -0,0 +1,62 @@ +/* + * 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/WState.hpp" + +#include "mqt/Dialect/QC/Builder/QCProgramBuilder.h" + +#include "Programs.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Tensor/IR/Tensor.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Value.h" +#include "mlir/IR/ValueRange.h" +#include "mlir/Support/LLVM.h" + +#include "llvm/ADT/ArrayRef.h" + +#include +#include +#include +#include + +namespace mqt::bench { +using namespace mlir; + +SmallVector wState(qc::QCProgramBuilder& b, const WState& benchmark) { + const auto size = static_cast(benchmark.options().qubits); + auto q = b.allocQubitRegisterStorage(size, "q"); + auto result = b.allocClassicalBitRegister(size, benchmark.output().name); + b.x(b.loadQubit(q, b.indexConstant(0))); + if (size > 1) { + std::vector angles(static_cast(size - 1)); + for (size_t i = 0; i < angles.size(); ++i) { + angles[i] = 2. * std::acos(1. / std::sqrt(static_cast(size) - + static_cast(i))); + } + const auto type = RankedTensorType::get({size - 1}, b.getF64Type()); + auto table = arith::ConstantOp::create( + b, DenseElementsAttr::get(type, ArrayRef(angles))); + auto one = b.indexConstant(1); + b.scfFor(0, size - 1, 1, [&](Value index) { + auto next = arith::AddIOp::create(b, index, one); + auto left = b.loadQubit(q, index); + auto right = b.loadQubit(q, next); + auto angle = tensor::ExtractOp::create(b, table, ValueRange{index}); + b.cry(angle, left, right); + b.cx(right, left); + }); + } + b.measureQubitRegister(q, result, size); + return {result}; +} +} // namespace mqt::bench diff --git a/mlir/include/mqt/Dialect/QCO/Utils/DDFunctionality.h b/mlir/include/mqt/Dialect/QCO/Utils/DDFunctionality.h index 20a6011878..0ce0a025bb 100644 --- a/mlir/include/mqt/Dialect/QCO/Utils/DDFunctionality.h +++ b/mlir/include/mqt/Dialect/QCO/Utils/DDFunctionality.h @@ -73,7 +73,7 @@ FailureOr buildFunctionality( /// In addition to the operations supported by `buildFunctionality`, simulation /// supports measurements, resets, CBit registers, and runtime qubit and QTensor /// allocation. QCO and SCF structured control requires concrete values. A -/// shared 10000-step limit bounds loops and calls. `qco.sink` and +/// shared 100-million-step limit bounds loops and calls. `qco.sink` and /// `qtensor.dealloc` mark lifetimes but do not remove DD wires. /// /// The containing module must pass MLIR verification and diff --git a/mlir/include/mqt/bench/Generate.h b/mlir/include/mqt/bench/Generate.h index 49bfb50c3a..fb13fdd4bd 100644 --- a/mlir/include/mqt/bench/Generate.h +++ b/mlir/include/mqt/bench/Generate.h @@ -20,6 +20,7 @@ namespace mqt::bench { class BV; class ModularMultiplier; class GHZ; +class WState; class Grover; class Multiplexer; class QFT; @@ -43,6 +44,9 @@ struct GeneratedBenchmark { [[nodiscard]] std::optional generate(const ModularMultiplier& benchmark); +/// Generate W-state preparation. +[[nodiscard]] std::optional generate(const WState& benchmark); + /// Generate the QC program for a configured GHZ benchmark. [[nodiscard]] std::optional generate(const GHZ& benchmark); diff --git a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp index d5e0e279cd..ee44f02bfd 100644 --- a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp +++ b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp @@ -74,7 +74,7 @@ namespace mlir::qco { namespace { -constexpr size_t MAX_CONTROL_FLOW_STEPS = 10'000; +constexpr size_t MAX_CONTROL_FLOW_STEPS = 100'000'000; struct QubitMap { DenseMap qubits; @@ -207,8 +207,8 @@ struct SamplingPlan { static LogicalResult consumeExecutionStep(WalkState& walk, Operation* op) { if (walk.remainingExecutionSteps == 0) { - return op->emitError( - "QCO DD execution exceeds the limit of 10000 control-flow steps"); + return op->emitError("QCO DD execution exceeds the limit of ") + << MAX_CONTROL_FLOW_STEPS << " control-flow steps"; } --walk.remainingExecutionSteps; return success(); @@ -1631,6 +1631,11 @@ static LogicalResult applyOp(Operation& op, WalkState& walk, StateDD& state) { return failure(); } + if (range->trips > walk.remainingExecutionSteps) { + return forOp.emitError("QCO DD execution exceeds the limit of ") + << MAX_CONTROL_FLOW_STEPS << " control-flow steps"; + } + Block& body = *forOp.getBody(); SmallVector carried(forOp.getInits().begin(), forOp.getInits().end()); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp index 66e056b278..6b529e45cf 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp @@ -2058,7 +2058,8 @@ TEST_F(QCODDFunctionalityTest, HandlesScfForBounds) { for (const auto [lower, upper, step, succeeds] : { std::tuple{3, 3, 1, true}, {0, 10000, 1, true}, - {0, 10001, 1, false}, + {0, 10001, 1, true}, + {0, 100000001, 1, false}, {0, 3, 0, false}, {0, 3, -1, false}, }) { @@ -2156,9 +2157,9 @@ TEST_F(QCODDFunctionalityTest, ScfForSharesExecutionBudget) { auto mod = buildModule([](QCOProgramBuilder& b) { auto q = b.staticQubit(0); auto outer = b.scfFor( - 0, 100, 1, ValueRange{q}, + 0, 2, 1, ValueRange{q}, [&](Value /*iv*/, ValueRange outerArgs) -> SmallVector { - return b.scfFor(0, 100, 1, outerArgs, + return b.scfFor(0, 100000000, 1, outerArgs, [&](Value /*innerIv*/, ValueRange innerArgs) -> SmallVector { return {innerArgs[0]}; }); }); @@ -2179,28 +2180,28 @@ TEST_F(QCODDFunctionalityTest, ExecutionBudgetIncludesBranchesAndCalls) { func.func @main() { %true = arith.constant true %zero = arith.constant 0 : index - %limit = arith.constant 10000 : index + %limit = arith.constant 100000000 : index %one = arith.constant 1 : index - scf.for %i = %zero to %limit step %one { - scf.if %true { + scf.if %true { + scf.for %i = %zero to %limit step %one { } } return } })mlir", R"mlir(module { - func.func @noop() { - return - } - func.func @main() { + func.func @loop() { %zero = arith.constant 0 : index - %limit = arith.constant 10000 : index + %limit = arith.constant 100000000 : index %one = arith.constant 1 : index scf.for %i = %zero to %limit step %one { - func.call @noop() : () -> () } return } + func.func @main() { + func.call @loop() : () -> () + return + } })mlir", }) { expectMlirSimulationFails(0, source); diff --git a/mlir/unittests/bench/CMakeLists.txt b/mlir/unittests/bench/CMakeLists.txt index 7e75429cb9..7a9427ca25 100644 --- a/mlir/unittests/bench/CMakeLists.txt +++ b/mlir/unittests/bench/CMakeLists.txt @@ -18,7 +18,8 @@ add_executable( test_benchmark_generate_qft_adder.cpp test_benchmark_generate_qpe.cpp test_benchmark_generate_repeat_until_success.cpp - test_benchmark_generate_teleportation.cpp) + test_benchmark_generate_teleportation.cpp + test_benchmark_generate_w_state.cpp) target_link_libraries(mqt-core-mlir-unittests-benchmark PRIVATE GTest::gtest_main MQT::CoreBenchGenerate MLIRQCODDFunctionality) diff --git a/mlir/unittests/bench/test_benchmark_cli.cmake b/mlir/unittests/bench/test_benchmark_cli.cmake index 8dac65e661..1414f4525d 100644 --- a/mlir/unittests/bench/test_benchmark_cli.cmake +++ b/mlir/unittests/bench/test_benchmark_cli.cmake @@ -49,8 +49,8 @@ endif() run_success("benchmark listing" list_output "${CLI}" list) run_failure("unknown benchmark" "${CLI}" describe unknown) string(JSON benchmark_count LENGTH "${list_output}" benchmarks) -if(NOT benchmark_count EQUAL 10) - message(FATAL_ERROR "list returned ${benchmark_count} benchmarks instead of 10") +if(NOT benchmark_count EQUAL 11) + message(FATAL_ERROR "list returned ${benchmark_count} benchmarks instead of 11") endif() run_success("multiplexer description" describe_output "${CLI}" describe multiplexer) @@ -288,3 +288,34 @@ file(GLOB invalid_outputs "${invalid_directory}/*") if(invalid_outputs) message(FATAL_ERROR "an invalid instance specification left a final output") endif() + +set(w_specification "${OUTPUT_DIR}/w-state.json") +file(WRITE "${w_specification}" + "{\"schema_version\":1,\"benchmark\":\"w-state\",\"parameters\":{\"qubits\":2}}\n") +run_success( + "W-state generation" + w_output + "${CLI}" + generate + --instance-specification + "${w_specification}" + --format + jeff + --output + "${OUTPUT_DIR}/w-state") +string(JSON w_manifest GET "${w_output}" manifest_path) +set(w_counts "${OUTPUT_DIR}/w-counts.json") +file(WRITE "${w_counts}" "{\"schema_version\":1,\"counts\":{\"01\":10,\"10\":10}}\n") +run_success( + "W-state evaluation" + w_evaluation + "${CLI}" + evaluate + --manifest + "${w_manifest}" + --counts + "${w_counts}") +string(JSON w_fidelity GET "${w_evaluation}" metrics squared_hellinger_fidelity) +if(NOT w_fidelity EQUAL 1) + message(FATAL_ERROR "uniform W-state counts did not match the analytic distribution") +endif() diff --git a/mlir/unittests/bench/test_benchmark_generate_w_state.cpp b/mlir/unittests/bench/test_benchmark_generate_w_state.cpp new file mode 100644 index 0000000000..1a705165f0 --- /dev/null +++ b/mlir/unittests/bench/test_benchmark_generate_w_state.cpp @@ -0,0 +1,102 @@ +/* + * 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/WState.hpp" +#include "dd/ComplexValue.hpp" +#include "dd/Edge.hpp" +#include "dd/Package.hpp" +#include "dd/StateGeneration.hpp" +#include "mqt/Compiler/Programs.h" +#include "mqt/Dialect/MQT/IR/MQTDialect.h" +#include "mqt/Dialect/QCO/Utils/DDFunctionality.h" +#include "mqt/bench/Generate.h" + +#include "TestUtils.h" + +#include "gtest/gtest.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/Support/LLVM.h" + +#include +#include +#include +#include +#include + +namespace mqt::bench { + +TEST(GenerateProgramTest, WStatePreservesCoherenceAndStructuredLoops) { + for (const size_t qubits : {1U, 3U, 16U}) { + const WState benchmark{{.qubits = qubits}}; + auto qc = generate(benchmark); + ASSERT_TRUE(qc); + if (qubits > 1) { + EXPECT_GT(test::countOps(qc->module()), 0U); + const auto angles = test::angleTable(qc->module()); + ASSERT_TRUE(angles); + EXPECT_EQ(angles.getNumElements(), qubits - 1); + } + auto qco = qc->copy().intoQCO(); + ASSERT_TRUE(qco); + const auto before = qco->str(); + dd::Package package(0); + const auto root = mlir::qco::simulateStatevector( + mlir::mqt::getEntryPoint(qco->module()), package); + ASSERT_TRUE(mlir::succeeded(root)); + EXPECT_EQ(qco->str(), before); + qco.reset(); + EXPECT_NEAR(package.fidelity(*root, dd::makeWState(qubits, package)), 1., + 1e-10); + for (size_t wire = 0; wire < qubits; ++wire) { + auto outcome = std::string(qubits, '0'); + outcome[qubits - wire - 1] = '1'; + const auto amplitude = root->getValueByPath(qubits, outcome); + EXPECT_NEAR(amplitude.real(), 1. / std::sqrt(static_cast(qubits)), + 1e-10); + EXPECT_NEAR(amplitude.imag(), 0., 1e-10); + } + test::expectJeffRoundTrip(std::move(*qc)); + test::expectSamplingMatchesReference(benchmark); + } +} + +TEST(GenerateProgramTest, Simulates4096QubitWStateWithoutDenseExtraction) { + constexpr size_t qubits = 4096; + const WState benchmark{{.qubits = qubits}}; + auto qc = generate(benchmark); + ASSERT_TRUE(qc); + EXPECT_LT(test::countOperations(qc->module()), 100U); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + auto jeff = std::move(*qco).intoJeff(); + ASSERT_TRUE(jeff); + auto restored = mlir::JeffProgram::fromBytes(jeff->toBytes()); + ASSERT_TRUE(restored); + qco = std::move(*restored).intoQCO(); + ASSERT_TRUE(qco); + dd::Package package(0); + const auto root = mlir::qco::simulateStatevector( + mlir::mqt::getEntryPoint(qco->module()), package); + ASSERT_TRUE(mlir::succeeded(root)); + EXPECT_EQ(package.qubits(), qubits); + EXPECT_NEAR(package.fidelity(*root, dd::makeWState(qubits, package)), 1., + 1e-8); + EXPECT_NEAR(package.innerProduct(*root, *root).r, 1., 1e-8); + for (const size_t index : {0U, 2047U, 4095U}) { + auto outcome = std::string(qubits, '0'); + outcome[index] = '1'; + const auto amplitude = root->getValueByPath(qubits, outcome); + EXPECT_NEAR(amplitude.real(), 1. / 64., 1e-8); + EXPECT_NEAR(amplitude.imag(), 0., 1e-8); + } +} + +} // namespace mqt::bench diff --git a/python/mqt/core/bench/__init__.pyi b/python/mqt/core/bench/__init__.pyi index acceb3f53b..7f4a71cbb1 100644 --- a/python/mqt/core/bench/__init__.pyi +++ b/python/mqt/core/bench/__init__.pyi @@ -18,6 +18,7 @@ from mqt.core.bench import qft_adder as qft_adder from mqt.core.bench import qpe as qpe from mqt.core.bench import repeat_until_success as repeat_until_success from mqt.core.bench import teleportation as teleportation +from mqt.core.bench import w_state as w_state class Output: """One logical classical output register.""" diff --git a/python/mqt/core/bench/w_state.pyi b/python/mqt/core/bench/w_state.pyi new file mode 100644 index 0000000000..7f5024d691 --- /dev/null +++ b/python/mqt/core/bench/w_state.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 + +"""W-state preparation instances and options.""" + +from collections.abc import Mapping + +import mqt.core.bench +import mqt.core.mlir + +class Options: + """Parameters for W-state preparation.""" + + def __init__(self, *, qubits: int) -> None: ... + @property + def qubits(self) -> int: + """The number of qubits.""" + +class WState: + """A validated W-state preparation 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.""" + + 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 = "") -> WState: + """Parse a strict benchmark instance specification.""" + + @staticmethod + def from_manifest_json(json: str, *, source: str = "") -> WState: + """Parse a strict benchmark manifest.""" diff --git a/src/bench/JSON.cpp b/src/bench/JSON.cpp index 8e0e313d8b..4003acf0cc 100644 --- a/src/bench/JSON.cpp +++ b/src/bench/JSON.cpp @@ -21,6 +21,7 @@ #include "bench/QPE.hpp" #include "bench/RepeatUntilSuccess.hpp" #include "bench/Teleportation.hpp" +#include "bench/WState.hpp" #include "SHA256.hpp" @@ -353,6 +354,18 @@ parseModularMultiplierParameters(const Json& parameters, }); } +[[nodiscard]] WState parseWStateParameters(const Json& parameters, + const std::string_view source) { + rejectUnknownKeys(parameters, {"qubits"}, source, "$/parameters"); + return constructBenchmark(source, [&] { + return WState({ + .qubits = + sizeValue(required(parameters, "qubits", source, "$/parameters"), + source, "$/parameters/qubits"), + }); + }); +} + [[nodiscard]] GHZ parseGHZParameters(const Json& parameters, const std::string_view source) { rejectUnknownKeys(parameters, {"qubits", "topology", "basis"}, source, @@ -575,6 +588,10 @@ parseTeleportationParameters(const Json& parameters, }; } +[[nodiscard]] Json parametersJSON(const WState& benchmark) { + return {{"qubits", benchmark.options().qubits}}; +} + [[nodiscard]] Json parametersJSON(const GHZ& benchmark) { const auto& options = benchmark.options(); return { @@ -670,6 +687,10 @@ parseTeleportationParameters(const Json& parameters, benchmark.expectedResult()); } +[[nodiscard]] Json referenceJSON(const WState& benchmark) { + return analyticReferenceJSON(benchmark.output(), "w_state"); +} + [[nodiscard]] Json referenceJSON(const GHZ& benchmark) { return analyticReferenceJSON(benchmark.output(), "ghz"); } @@ -869,6 +890,27 @@ template }); } +[[nodiscard]] Json wStateInstanceSpecificationSchema() { + return baseInstanceSpecificationSchema({ + {"additionalProperties", false}, + {"type", "object"}, + {"required", {"qubits"}}, + { + "properties", + { + { + "qubits", + { + {"type", "integer"}, + {"minimum", 1}, + {"maximum", std::numeric_limits::max()}, + }, + }, + }, + }, + }); +} + [[nodiscard]] Json ghzInstanceSpecificationSchema() { Json parameters{ {"additionalProperties", false}, diff --git a/src/bench/WState.cpp b/src/bench/WState.cpp new file mode 100644 index 0000000000..ac7cfd2809 --- /dev/null +++ b/src/bench/WState.cpp @@ -0,0 +1,49 @@ +/* + * 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/WState.hpp" + +#include "bench/Evaluation.hpp" + +#include "EvaluationUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::bench { + +WState::WState(const WStateOptions options) + : options_(options), output_{.name = "result", .width = options.qubits} { + if (options.qubits == 0 || + options.qubits > + static_cast(std::numeric_limits::max()) || + options.qubits - 1 > std::vector{}.max_size()) { + throw std::invalid_argument("W-state qubits must be positive and fit " + "circuit dimensions and angle storage"); + } +} +const WStateOptions& WState::options() const noexcept { return options_; } +const Output& WState::output() const noexcept { return output_; } +double WState::probability(const std::string_view outcome) const { + detail::validateOutcome(outcome, output_.width); + return std::ranges::count(outcome, '1') == 1 + ? 1. / static_cast(options_.qubits) + : 0.; +} +Evaluation WState::evaluate(const Counts& counts) const { + return detail::evaluate(*this, counts); +} + +} // namespace mqt::bench diff --git a/test/bench/test_json.cpp b/test/bench/test_json.cpp index b9c9ccde3d..20dcbae96d 100644 --- a/test/bench/test_json.cpp +++ b/test/bench/test_json.cpp @@ -575,7 +575,7 @@ 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":"modular-multiplier"},{"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":"repeat-until-success"},{"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":"modular-multiplier"},{"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":"repeat-until-success"},{"definition_version":1,"id":"teleportation"},{"definition_version":1,"id":"w-state"}],"schema_version":1})"); const auto bv = describeBenchmarkJSON("bv"); const auto modularMultiplier = describeBenchmarkJSON("modular-multiplier"); const auto ghz = describeBenchmarkJSON("ghz"); diff --git a/test/bench/test_w_state.cpp b/test/bench/test_w_state.cpp new file mode 100644 index 0000000000..56f6200cad --- /dev/null +++ b/test/bench/test_w_state.cpp @@ -0,0 +1,88 @@ +/* + * 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/JSON.hpp" +#include "bench/WState.hpp" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +namespace mqt::bench { + +TEST(WState, AssignsUniformProbabilityToSingleExcitations) { + for (const size_t qubits : {1U, 2U, 3U, 7U}) { + const WState benchmark{{.qubits = qubits}}; + EXPECT_EQ(benchmark.options().qubits, qubits); + EXPECT_EQ(benchmark.output(), (Output{"result", qubits})); + Counts uniform; + for (size_t index = 0; index < (size_t{1} << qubits); ++index) { + auto bits = std::string(qubits, '0'); + for (size_t bit = 0; bit < qubits; ++bit) { + if ((index & (size_t{1} << bit)) != 0) { + bits[qubits - bit - 1] = '1'; + } + } + const auto expected = + std::popcount(index) == 1 ? 1. / static_cast(qubits) : 0.; + EXPECT_DOUBLE_EQ(benchmark.probability(bits), expected); + if (expected != 0.) { + uniform.emplace(bits, 10); + } + } + const auto evaluation = benchmark.evaluate(uniform); + EXPECT_NEAR(evaluation.totalVariationDistance, 0., 1e-12); + EXPECT_NEAR(evaluation.squaredHellingerFidelity, 1., 1e-12); + } + const auto partial = WState{{.qubits = 2}}.evaluate({{"01", 20}}); + EXPECT_NEAR(partial.totalVariationDistance, 0.5, 1e-12); + EXPECT_NEAR(partial.squaredHellingerFidelity, 0.5, 1e-12); +} + +TEST(WState, RejectsInvalidParametersAndOutcomes) { + EXPECT_THROW(static_cast(WState{{.qubits = 0}}), std::invalid_argument); + EXPECT_THROW( + static_cast(WState{{.qubits = std::numeric_limits::max()}}), + std::invalid_argument); + const WState benchmark{{.qubits = 2}}; + EXPECT_THROW(static_cast(benchmark.probability("1")), + std::invalid_argument); + EXPECT_THROW(static_cast(benchmark.evaluate({{"0x", 1}})), + std::invalid_argument); +} + +TEST(WState, RoundTripsStrictManifestsAndEvaluatesCounts) { + const auto schema = describeBenchmarkJSON("w-state"); + EXPECT_NE(schema.find(R"("minimum":1)"), std::string::npos); + EXPECT_NE(schema.find(R"("required":["qubits"])"), std::string::npos); + const WState benchmark{{.qubits = 2}}; + const auto manifest = toManifestJSON(benchmark); + EXPECT_EQ(caseId(wStateFromInstanceSpecificationJSON( + toInstanceSpecificationJSON(benchmark))), + caseId(benchmark)); + EXPECT_EQ(toManifestJSON(wStateFromManifestJSON(manifest)), manifest); + EXPECT_NE(manifest.find(R"("model":"w_state")"), std::string::npos); + const auto evaluation = evaluateJSON( + manifest, R"({"schema_version":1,"counts":{"01":10,"10":10}})"); + EXPECT_NE(evaluation.find(R"("squared_hellinger_fidelity":1.0)"), + std::string::npos); + EXPECT_NE(evaluation.find(R"("shots":20)"), std::string::npos); + EXPECT_THROW( + static_cast(wStateFromInstanceSpecificationJSON( + R"({"schema_version":1,"benchmark":"w-state","parameters":{"qubits":0}})")), + std::invalid_argument); +} + +} // namespace mqt::bench diff --git a/test/python/bench/test_w_state.py b/test/python/bench/test_w_state.py new file mode 100644 index 0000000000..fb96de7349 --- /dev/null +++ b/test/python/bench/test_w_state.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 + +"""W-state generation and evaluation through the existing benchmark API.""" + +from __future__ import annotations + +import json + +import pytest + +from mqt.core import mlir +from mqt.core.bench import w_state + + +def test_w_state_reference_and_json() -> None: + """Expose the new family's parameters, ideal probabilities, and manifest.""" + benchmark = w_state.WState(w_state.Options(qubits=3)) + assert benchmark.options.qubits == 3 + assert benchmark.output.name == "result" + assert benchmark.output.width == 3 + assert benchmark.probability("010") == pytest.approx(1 / 3) + assert benchmark.probability("111") == 0 + counts = {"001": 10, "010": 10, "100": 10} + evaluation = benchmark.evaluate(counts) + assert evaluation.total_variation_distance == pytest.approx(0) + assert evaluation.squared_hellinger_fidelity == pytest.approx(1) + manifest = json.loads(benchmark.manifest_json) + assert manifest["reference"]["model"] == "w_state" + assert w_state.WState.from_manifest_json(benchmark.manifest_json).case_id == benchmark.case_id + assert ( + w_state.WState.from_instance_specification_json(benchmark.instance_specification_json).manifest_json + == benchmark.manifest_json + ) + with pytest.raises(ValueError, match="qubits must be positive"): + w_state.WState(w_state.Options(qubits=0)) + + +def test_w_state_generation_and_sampling() -> None: + """Sample W states through QC and serialized jeff programs.""" + assert mlir.sample(w_state.WState(w_state.Options(qubits=1)).generate(), shots=32) == {"1": 32} + benchmark = w_state.WState(w_state.Options(qubits=3)) + qc = benchmark.generate() + jeff = qc.to_qco(copy=True).to_jeff() + for program in (qc, mlir.JeffProgram.from_bytes(jeff.to_bytes())): + counts = mlir.sample(program, shots=4096, seed=17) + assert set(counts) == {"001", "010", "100"} + assert sum(counts.values()) == 4096 + assert benchmark.evaluate(counts).total_variation_distance < 0.03