diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff91510405..f365d82b98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: ## Check JSON schemata - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.37.2 + rev: 0.37.3 hooks: - id: check-github-workflows priority: 0 @@ -47,14 +47,14 @@ repos: ## Check pyproject.toml file - repo: https://github.com/henryiii/validate-pyproject-schema-store - rev: 2026.05.28 + rev: 2026.06.12 hooks: - id: validate-pyproject priority: 0 ## Ensure uv.lock is up to date - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.11.19 + rev: 0.11.21 hooks: - id: uv-lock priority: 0 @@ -104,7 +104,7 @@ repos: ## Format configuration files with prettier - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.8.3 + rev: v3.8.4 hooks: - id: prettier types_or: [yaml, html, css, scss, javascript, json, json5] @@ -145,7 +145,7 @@ repos: ## Format and lint Python files with ruff - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.16 + rev: v0.15.17 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] @@ -156,7 +156,7 @@ repos: ## Check Python types with ty - repo: https://github.com/astral-sh/ty-pre-commit - rev: v0.0.47 + rev: v0.0.49 hooks: - id: ty args: [--only-dev] diff --git a/CHANGELOG.md b/CHANGELOG.md index eb58acb8be..ef9c0a36f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ with the exception that minor releases may include breaking changes. ### Added +- ✨ Add an `absorb-swaps` pass for absorbing initial SWAPs ([#1750]) + ([**@jmoosburger**], [**@MatthiasReumann**], [**@burgholzer**]) - ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis ([#1672]) ([**@simon1hofmann**], [**@burgholzer**]) @@ -608,6 +610,7 @@ changelogs._ [#1765]: https://github.com/munich-quantum-toolkit/core/pull/1765 [#1762]: https://github.com/munich-quantum-toolkit/core/pull/1762 [#1751]: https://github.com/munich-quantum-toolkit/core/pull/1751 +[#1750]: https://github.com/munich-quantum-toolkit/core/pull/1750 [#1749]: https://github.com/munich-quantum-toolkit/core/pull/1749 [#1748]: https://github.com/munich-quantum-toolkit/core/pull/1748 [#1737]: https://github.com/munich-quantum-toolkit/core/pull/1737 @@ -856,6 +859,7 @@ changelogs._ [**@keefehuang**]: https://github.com/keefehuang [**@J4MMlE**]: https://github.com/J4MMlE [**@rturrado**]: https://github.com/rturrado +[**@jmoosburger**]: https://github.com/jmoosburger diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index fafb906a77..2690f13b44 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -200,4 +200,18 @@ def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> { }]; } +def SwapAbsorption : Pass<"absorb-swaps", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect"]; + let summary = "This pass absorbs SWAP operations into the initial " + "program-to-hardware mapping."; + let description = [{ + For a SWAP operation exchanging static qubits q0 and q1, the pass replaces the use of the + first (second) input qubit with the second (first) output qubit of the SWAP and subsequently + removes the operation. As a result, the initial program-to-hardware mapping is changed. + This process is repeated until no more SWAP operations can be absorbed. + + The pass assumes that the quantum program is already mapped to static qubits. + }]; +} + #endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/SwapAbsorption.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/SwapAbsorption.cpp new file mode 100644 index 0000000000..c6c7e78c00 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/SwapAbsorption.cpp @@ -0,0 +1,74 @@ +/* + * 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 "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Drivers.h" +#include "mlir/Dialect/QCO/Utils/WireIterator.h" + +#include +#include +#include +#include +#include + +namespace mlir::qco { +#define GEN_PASS_DEF_SWAPABSORPTION +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { +struct SwapAbsorption : impl::SwapAbsorptionBase { + using SwapAbsorptionBase::SwapAbsorptionBase; + +protected: + void runOnOperation() override { + ModuleOp anchor = getOperation(); + IRRewriter rewriter(&getContext()); + + for (auto func : anchor.getOps()) { + SmallVector readyToAbsorb; + SmallVector wires; + do { + wires.clear(); + for (auto op : func.getOps()) { + wires.emplace_back(op.getQubit()); + } + if (wires.empty()) { + return; + } + + readyToAbsorb.clear(); + findSwapsReadyForAbsorption(wires, readyToAbsorb); + + for (auto swapOp : readyToAbsorb) { + rewriter.replaceOp(swapOp, + {swapOp.getQubit1In(), swapOp.getQubit0In()}); + } + } while (!readyToAbsorb.empty()); + } + } + +private: + static void findSwapsReadyForAbsorption(MutableArrayRef wires, + SmallVector& readyToAbsorb) { + std::ignore = walkProgramGraph( + wires, [&](const ReadyRange& ready, ReleasedOps& released) { + for (const auto& [op, indices] : ready) { + if (isa(op)) { + readyToAbsorb.emplace_back(op); + } + released.emplace_back(op); + } + return WalkResult::interrupt(); + }); + } +}; +} // namespace +} // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt index e80e957680..47a33d0b01 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt @@ -7,8 +7,9 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-optimizations) -add_executable(${target_name} test_qco_hadamard_lifting.cpp - test_qco_merge_single_qubit_rotation.cpp test_quantum_loop_unroll.cpp) +add_executable( + ${target_name} test_qco_hadamard_lifting.cpp test_qco_merge_single_qubit_rotation.cpp + test_quantum_loop_unroll.cpp test_swapabsorption.cpp) target_link_libraries( ${target_name} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_swapabsorption.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_swapabsorption.cpp new file mode 100644 index 0000000000..9f46daa371 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_swapabsorption.cpp @@ -0,0 +1,189 @@ +/* + * 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 "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace mlir; +using namespace mlir::qco; + +namespace { + +class SwapAbsorbPassTest : public testing::Test { + +protected: + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + static void applySwapAbsorb(OwningOpRef& moduleOp) { + PassManager pm(moduleOp->getContext()); + pm.addPass(qco::createSwapAbsorption()); + auto res = pm.run(*moduleOp); + + ASSERT_TRUE(succeeded(res)); + } + + std::unique_ptr context; +}; +}; // namespace + +TEST_F(SwapAbsorbPassTest, PassDoesNotChangeSwaplessProgram) { + + qco::QCOProgramBuilder builder(context.get()); + builder.initialize(); + + const auto q00 = builder.staticQubit(0); + const auto q10 = builder.staticQubit(1); + + const auto q01 = builder.h(q00); + const auto [q02, q11] = builder.cx(q01, q10); + + builder.sink(q02); + builder.sink(q11); + + auto moduleThroughPass = builder.finalize(); + auto originalModule = moduleThroughPass->clone(); + + applySwapAbsorb(moduleThroughPass); + ASSERT_TRUE(mlir::OperationEquivalence::isEquivalentTo( + moduleThroughPass.get(), originalModule, + mlir::OperationEquivalence::Flags::IgnoreLocations)); +} + +TEST_F(SwapAbsorbPassTest, PassReordersTwoQubitCircuitWithLeadingSwap) { + + qco::QCOProgramBuilder builder(context.get()); + builder.initialize(); + + const auto q00 = builder.staticQubit(0); + const auto q10 = builder.staticQubit(1); + + const auto [q01, q11] = builder.swap(q00, q10); + + const auto q02 = builder.id(q01); + const auto q12 = builder.id(q11); + + builder.sink(q02); + builder.sink(q12); + + auto moduleThroughPass = builder.finalize(); + applySwapAbsorb(moduleThroughPass); + + ASSERT_EQ(q10, mlir::cast(q02.getDefiningOp()).getInputQubit(0)); + ASSERT_EQ(q00, mlir::cast(q12.getDefiningOp()).getInputQubit(0)); +} + +TEST_F(SwapAbsorbPassTest, PassAbsorbsTwoIndependentSwaps) { + + qco::QCOProgramBuilder builder(context.get()); + builder.initialize(); + + const auto q00 = builder.staticQubit(0); + const auto q10 = builder.staticQubit(1); + const auto q20 = builder.staticQubit(2); + const auto q30 = builder.staticQubit(3); + + const auto [q01, q11] = builder.swap(q00, q10); + const auto [q21, q31] = builder.swap(q20, q30); + + const auto q02 = builder.id(q01); + const auto q12 = builder.id(q11); + const auto q22 = builder.id(q21); + const auto q32 = builder.id(q31); + + builder.sink(q02); + builder.sink(q12); + builder.sink(q22); + builder.sink(q32); + + auto moduleThroughPass = builder.finalize(); + applySwapAbsorb(moduleThroughPass); + + ASSERT_EQ(q10, mlir::cast(q02.getDefiningOp()).getInputQubit(0)); + ASSERT_EQ(q00, mlir::cast(q12.getDefiningOp()).getInputQubit(0)); + ASSERT_EQ(q30, mlir::cast(q22.getDefiningOp()).getInputQubit(0)); + ASSERT_EQ(q20, mlir::cast(q32.getDefiningOp()).getInputQubit(0)); +} + +TEST_F(SwapAbsorbPassTest, PassAbsorbsSwapWithLeadingSingleQubitGates) { + + qco::QCOProgramBuilder builder(context.get()); + builder.initialize(); + + const auto q00 = builder.staticQubit(0); + const auto q10 = builder.staticQubit(1); + + const auto q01 = builder.id(q00); + const auto q11 = builder.id(q10); + + const auto [q02, q12] = builder.swap(q01, q11); + + const auto q03 = builder.id(q02); + const auto q13 = builder.id(q12); + + builder.sink(q03); + builder.sink(q13); + + auto moduleThroughPass = builder.finalize(); + applySwapAbsorb(moduleThroughPass); + + ASSERT_EQ(q11, mlir::cast(q03.getDefiningOp()).getInputQubit(0)); + ASSERT_EQ(q01, mlir::cast(q13.getDefiningOp()).getInputQubit(0)); +} + +TEST_F(SwapAbsorbPassTest, PassAbsorbsTwoDependentSwaps) { + + qco::QCOProgramBuilder builder(context.get()); + builder.initialize(); + + const auto q00 = builder.staticQubit(0); + const auto q10 = builder.staticQubit(1); + const auto q20 = builder.staticQubit(2); + + const auto [q01, q11] = builder.swap(q00, q10); + const auto [q12, q21] = builder.swap(q11, q20); + + const auto q02 = builder.id(q01); + const auto q13 = builder.id(q12); + const auto q22 = builder.id(q21); + + builder.sink(q02); + builder.sink(q13); + builder.sink(q22); + + auto moduleThroughPass = builder.finalize(); + applySwapAbsorb(moduleThroughPass); + + ASSERT_EQ(q20, mlir::cast(q13.getDefiningOp()).getInputQubit(0)); + ASSERT_EQ(q00, mlir::cast(q22.getDefiningOp()).getInputQubit(0)); + ASSERT_EQ(q10, mlir::cast(q02.getDefiningOp()).getInputQubit(0)); +} diff --git a/test/algorithms/test_qpe.cpp b/test/algorithms/test_qpe.cpp index 553f3dfac1..287b658b70 100644 --- a/test/algorithms/test_qpe.cpp +++ b/test/algorithms/test_qpe.cpp @@ -175,7 +175,7 @@ TEST_P(QPE, IQPETest) { auto qc = qc::createIterativeQPE(lambda, precision); ASSERT_EQ(qc.getNqubits(), 2U); - constexpr auto shots = 8192U; + constexpr auto shots = 16384U; const auto measurements = dd::sample(qc, shots); // sort the measurements diff --git a/test/qdmi/devices/dd/device_status_test.cpp b/test/qdmi/devices/dd/device_status_test.cpp index 67054e518a..2943ebf93c 100644 --- a/test/qdmi/devices/dd/device_status_test.cpp +++ b/test/qdmi/devices/dd/device_status_test.cpp @@ -39,9 +39,9 @@ TEST(DeviceStatus, TransitionsBusyThenIdleAfterJob) { // it. Submit a job to force BUSY then completion to IDLE. const qdmi_test::JobGuard j{s.session}; ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::QASM3_BELL_SAMPLING), + qdmi_test::QASM3_HEAVY_SAMPLING), QDMI_SUCCESS); - ASSERT_EQ(qdmi_test::setShots(j.job, 4096), QDMI_SUCCESS); + ASSERT_EQ(qdmi_test::setShots(j.job, 16384), QDMI_SUCCESS); ASSERT_EQ(MQT_DDSIM_QDMI_device_job_submit(j.job), QDMI_SUCCESS); // Poll while running to observe BUSY at least once. @@ -65,30 +65,3 @@ TEST(DeviceStatus, TransitionsBusyThenIdleAfterJob) { // After completion, the status should be IDLE. EXPECT_EQ(queryStatus(s.session), QDMI_DEVICE_STATUS_IDLE); } - -TEST(DeviceStatus, MultipleConcurrentJobsKeepBusyUntilLastFinishes) { - const qdmi_test::SessionGuard s{}; - - const qdmi_test::JobGuard j1{s.session}; - const qdmi_test::JobGuard j2{s.session}; - ASSERT_EQ(qdmi_test::setProgram(j1.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::QASM3_BELL_SAMPLING), - QDMI_SUCCESS); - ASSERT_EQ(qdmi_test::setProgram(j2.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::QASM3_HEAVY_SAMPLING5), - QDMI_SUCCESS); - ASSERT_EQ(qdmi_test::setShots(j1.job, 1024), QDMI_SUCCESS); - ASSERT_EQ(qdmi_test::setShots(j2.job, 16384), QDMI_SUCCESS); - - ASSERT_EQ(MQT_DDSIM_QDMI_device_job_submit(j1.job), QDMI_SUCCESS); - ASSERT_EQ(MQT_DDSIM_QDMI_device_job_submit(j2.job), QDMI_SUCCESS); - - // Wait for first to finish - ASSERT_EQ(MQT_DDSIM_QDMI_device_job_wait(j1.job, 0), QDMI_SUCCESS); - // Status should still be BUSY while the second runs - EXPECT_EQ(queryStatus(s.session), QDMI_DEVICE_STATUS_BUSY); - - // Wait for second, then status should go IDLE - ASSERT_EQ(MQT_DDSIM_QDMI_device_job_wait(j2.job, 0), QDMI_SUCCESS); - EXPECT_EQ(queryStatus(s.session), QDMI_DEVICE_STATUS_IDLE); -} diff --git a/test/qdmi/devices/dd/error_handling_test.cpp b/test/qdmi/devices/dd/error_handling_test.cpp index b2766c051a..c67f1ec19b 100644 --- a/test/qdmi/devices/dd/error_handling_test.cpp +++ b/test/qdmi/devices/dd/error_handling_test.cpp @@ -75,9 +75,9 @@ TEST_F(ErrorHandling, GetResultsBeforeDone) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::QASM3_BELL_SAMPLING), + qdmi_test::QASM3_HEAVY_SAMPLING), QDMI_SUCCESS); - ASSERT_EQ(qdmi_test::setShots(j.job, 64), QDMI_SUCCESS); + ASSERT_EQ(qdmi_test::setShots(j.job, 16384), QDMI_SUCCESS); // Before submit → invalid EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( j.job, QDMI_JOB_RESULT_HIST_KEYS, 0, nullptr, nullptr), diff --git a/test/qdmi/devices/dd/helpers/circuits.hpp b/test/qdmi/devices/dd/helpers/circuits.hpp index 156dd1de94..b6f7564ff1 100644 --- a/test/qdmi/devices/dd/helpers/circuits.hpp +++ b/test/qdmi/devices/dd/helpers/circuits.hpp @@ -32,9 +32,9 @@ cx q[0], q[1]; inline constexpr const char* QASM3_MALFORMED = "Definitely not OpenQASM"; -// A slightly heavier 5-qubit sampling circuit to prolong runtime slightly while +// A slightly heavier dynamic sampling circuit to prolong runtime slightly while // remaining fast -inline constexpr auto QASM3_HEAVY_SAMPLING5 = R"( +inline constexpr auto QASM3_HEAVY_SAMPLING = R"( OPENQASM 3; include "stdgates.inc"; qubit[5] q; @@ -58,6 +58,16 @@ cx q[1], q[2]; cx q[0], q[1]; // Measure all qubits c = measure q; +// Add dynamic component +if (c == 3) { + rx(0.7) q[0]; + ry(0.5) q[1]; + rz(1.1) q[2]; + ry(0.3) q[3]; + rx(0.9) q[4]; +} +// Measure all qubits again +c = measure q; )"; } // namespace qdmi_test diff --git a/test/qdmi/devices/dd/job_lifecycle_test.cpp b/test/qdmi/devices/dd/job_lifecycle_test.cpp index 267740641b..bf9be9282f 100644 --- a/test/qdmi/devices/dd/job_lifecycle_test.cpp +++ b/test/qdmi/devices/dd/job_lifecycle_test.cpp @@ -111,7 +111,7 @@ TEST(JobLifecycle, FreeWhileRunningWaitsForCompletion) { ASSERT_EQ(MQT_DDSIM_QDMI_device_session_create_device_job(s.session, &job), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::setProgram(job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::QASM3_HEAVY_SAMPLING5), + qdmi_test::QASM3_HEAVY_SAMPLING), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::setShots(job, 4096), QDMI_SUCCESS); ASSERT_EQ(MQT_DDSIM_QDMI_device_job_submit(job), QDMI_SUCCESS);