diff --git a/CHANGELOG.md b/CHANGELOG.md index 62934a1840..a915b60a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ releases may include breaking changes. #### Passes and transformations +- ✨ Add a `constant propagation` pass for reducing superfluous quantum + resources by propagating the quantum machine state ([#1845]) ([**@lirem101**]) - ✨ Add quantum loop unrolling and qubit reuse passes ([#1705], [#1718], [#1755], [#1756], [#1923], [#1924], [#2039], [#2118]) ([**@MatthiasReumann**], [**@DRovara**], [**@burgholzer**], [**@simon1hofmann**]) @@ -78,78 +80,125 @@ releases may include breaking changes. - ✨ Add extensible program serializers to QDMI Qiskit backends. [QDMI-on-IQM] now provides the IQM JSON serializer and `MoveGate` integration ([#2114]) ([**@marcelwa**]) -- 🐳 Add dev container configuration for a consistent local development - environment ([#1786]) ([**@denialhaag**]) - -### Changed - -- 💥 Prune dead and misleading CoreIR APIs and remove random-number generator - state from `QuantumComputation` ([#2111], [#2112]) ([**@simon1hofmann**]) -- 💥 Update QIR execution for QIR 2.1, isolated runtimes, reproducible - multi-shot execution, and safe statevector extraction ([#2035], [#2036]) - ([**@burgholzer**]) -- 💥 Require LLVM/MLIR 22.1 and QIR support in every MQT Core source build, - build MLIR by default, and remove the corresponding build options ([#1356], - [#1549], [#1953]) ([**@burgholzer**], [**@denialhaag**]) - -### Removed - -- 💥 Remove the unowned decision-diagram approximation algorithm and - density-matrix support from MQT Core ([#1466], [#2154]) ([**@burgholzer**]) -- 💥 Make `nlohmann_json` an implementation detail and replace JSON-typed - decision-diagram statistics APIs with strings and streams ([#2138]) - ([**@denialhaag**]) -- 💥 Remove the neutral-atom stack from MQT Core and move it to [MQT QMAP] - ([#2137]) ([**@denialhaag**]) -- 💥 Remove the FoMaC compatibility names from the C++ and Python QDMI APIs - ([#2115]) ([**@burgholzer**]) -- 💥 Remove the ZX-calculus library and its Boost.Multiprecision and GMP - support. Equivalence-checking users should use [MQT QCEC] ([#2082]) - ([**@burgholzer**]) -- 🔥 Remove `datastructures` (`ds`) (sub)library from MQT Core ([#1458]) - ([**@burgholzer**]) - -## [3.9.0] - 2026-08-19 - -_If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#390)._ - -### Added - -- ✨ Add PennyLane support for gate-based QDMI devices ([#2005], [#2147]) - ([**@burgholzer**], [**@marcelwa**]) +- ✨ Add `mqt.core.qdmi.is_binary_program_format`, which states whether a + program format requires exact-byte submission ([#2114]) ([**@marcelwa**]) - ✨ Add `Device::submitCalibrationJob` and `mqt.core.qdmi.Device.submit_calibration_job` for triggering a calibration run - ([#2148]) ([**@marcelwa**], [**@burgholzer**]) + ([#2148]) ([**@marcelwa**]) - ✨ Add SpecAudits, a method and probe script for auditing tests that pin behavior the project never specified ([#2124]) ([**@marcelwa**]) -- 🚸 Add typed stable-ID construction for Qiskit backends, lazy provider - discovery, and sampler and estimator factories with explicit shot and - precision defaults ([#2084]) ([**@burgholzer**]) -- 🧪 Test static Slurm license admission and QDMI execution with DDSIM and the - superconducting device, and document the cluster setup ([#2043]) +- ✨ Extract the existing QCO dead-gate elimination patterns from + canonicalization into an explicit `remove-dead-gates` pass and use it in the + qubit-reuse pipeline ([#2118]) ([**@simon1hofmann**]) +- 🚸 Add typed stable-ID construction for Qiskit backends and compiler targets, + fluent Qiskit primitives, and lazy provider discovery ([#2084]) ([**@burgholzer**]) -- ✨ Add C++ FoMaC and Python QDMI adapters that open the device named by one - local Slurm license environment value ([#2025]) ([**@burgholzer**]) -- ✨ Add generic C++ FoMaC and Python QDMI support for custom device properties - that contain operation handles ([#2042]) ([**@burgholzer**]) -- 📝 Generate `llms.txt` documentation indexes with Sphinx-LLM ([#1989], - [#2046]) ([**@denialhaag**], [**@burgholzer**]) -- ✨ Support retrieving existing jobs by ID through the QDMI client API, C++ - FoMaC API, and Python QDMI API, and expose optional device queue length and - job queue position ([#2008], [#2010]) ([**@burgholzer**]) -- 🐍 Build CPython 3.15 wheels. Their post-build tests remain disabled until - test dependency wheels are available ([#2011]) ([**@denialhaag**]) +- 🧪 Test static Slurm license admission and QDMI execution with DDSIM and SC, + and document the cluster setup ([#2043]) ([**@burgholzer**]) +- ✨ Add C++ and Python adapters that open the QDMI device named by one local + Slurm license environment value ([#2025]) ([**@burgholzer**]) +- ✨ Add an `unroll-modifiers` pass for unrolling multi-operation modifiers + ([#2015]) ([**@denialhaag**], [**@burgholzer**]) +- ✨ Add Qiskit circuit import and export to the compiler collection ([#2031], + [#2133], [#2136], [#2140]) ([**@burgholzer**], [**@simon1hofmann**]) +- ✨ Add generic C++ and Python FoMaC support for custom device properties that + contain operation handles ([#2042]) ([**@burgholzer**]) +- ✨ Support retrieving existing jobs by ID through the QDMI client API and C++ + and Python FoMaC APIs, and expose optional device queue length and job queue + position ([#2008], [#2010]) ([**@burgholzer**]) +- 🐍 Start building CPython 3.15 wheels ([#2011]) ([**@denialhaag**]) +- ✨ Add PennyLane support for gate-based QDMI devices ([#2005], [#2147]) + ([**@burgholzer**], [**@marcelwa**]) +- ✨ Integrate QDMI devices as MLIR compiler targets across C++, Python, and + `mqt-cc` ([#1687]) ([**@MatthiasReumann**], [**@burgholzer**]) +- ✨ Add structured OpenQASM emission from the QC dialect to the C++ and Python + compiler APIs and `mqt-cc` ([#2003]) ([**@burgholzer**]) +- ✨ Add immutable MLIR compiler targets and a canonical target compilation + pipeline for decomposition, optimization, mapping, native synthesis, and + conformance ([#1993], [#1999], [#2049]) ([**@simon1hofmann**], + [**@burgholzer**]) - ✨ Bundle reusable IQM Garnet and Emerald superconducting device models with stable QDMI registry IDs ([#1992]) ([**@burgholzer**]) +- ✨ Add compiler-wide scoped global-phase normalization with exact modifier + semantics and synthesis integration ([#1986], [#1995]) ([**@burgholzer**]) - ✨ Expose compressed vector and matrix DD serialization through bytes-based Python APIs ([#1983]) ([**@burgholzer**]) -- ✨ Make the neutral-atom and superconducting QDMI devices runtime configurable - with session-owned topology, operations, and calibration data ([#1974], - [#1980]) ([**@burgholzer**]) +- ✨ Make superconducting QDMI devices runtime configurable with session-owned + topology, operations, and calibration data ([#1980]) ([**@burgholzer**]) - ✨ Expose registered QDMI device IDs without loading device libraries ([#1972]) ([**@burgholzer**]) - ✨ Add typed runtime configuration transport and relocatable assets for QDMI device descriptions ([#1967]) ([**@burgholzer**]) +- ✨ Add and improve QIR generation support in the MQT Compiler Collection + ([#1264], [#1446], [#1513], [#1521], [#1548], [#1567], [#1569], [#1570], + [#1572], [#1580], [#1620], [#1624], [#1626], [#1648], [#1710], [#1751], + [#1755], [#1787], [#1815], [#1823], [#1830], [#1886], [#1933], [#1978], + [#1979], [#2007], [#2026], [#2030], [#2066]) ([**@burgholzer**], + [**@denialhaag**], [**@simon1hofmann**], [**@li-mingbao**], [**@DRovara**], + [**@MatthiasReumann**]) +- ✨ Add decision diagram-based construction, simulation, and sampling of QCO + functions, including static unitaries, mid-circuit `measure`/`reset`, concrete + `if`/`index_switch`, initial classical SSA evaluation, dense `k>3` wire + embedding, and multi-shot `sample` ([#1915], [#1973]) ([**@simon1hofmann**]) +- ✨ Add target-independent two-qubit gate fusion, target-native post-routing + synthesis, and operation-capability and static-site conformance ([#1865], + [#1961], [#1998]) ([**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add a `decompose-multi-controlled` pass for decomposing controlled X, Z, + SWAP, RCCX, and constant-angle phase gates with a configurable `min-qubits` + threshold (default 3: wider than two-qubit) ([#1810], [#1996], [#2001]) + ([**@simon1hofmann**]) +- ✨ Add an LLVM-native staged OpenQASM frontend with typed semantic analysis + and direct QC emission, including lexical scope, assignment, inclusive ranges, + structured control flow, and alias-safe qubit-register access ([#1910], + [#1987], [#1994], [#2026]) ([**@burgholzer**], [**@denialhaag**]) +- ✨ Add Python bindings for the MQT Compiler Collection ([#1815]) + ([**@burgholzer**], [**@denialhaag**]) +- ✨ Add support for IQM's `move` gate in the QDMI Qiskit backend converter + ([#1844], [#1848]) ([**@burgholzer**], [**@marcelwa**]) +- 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function + ([#1826]) ([**@ystade**]) +- 🐳 Add dev container configuration for consistent local development + environment ([#1786]) ([**@denialhaag**]) +- ✨ Add two-qubit Weyl (KAK) decomposition and native-gateset synthesis support + ([#1803], [#1832]) ([**@simon1hofmann**], [**@burgholzer**]) +- ✨ Extend the QCO unitary matrix library ([#1774], [#1802], [#1809], [#1814], + [#1850]) ([**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time + single-qubit unitary runs via Euler resynthesis ([#1672]) + ([**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add a `quantum-loop-unroll` pass for unrolling for-loop operations + containing quantum operations ([#1718]) ([**@MatthiasReumann**]) +- ✨ Add a `hadamard-lifting` pass for lifting Hadamard gates above Pauli gates + ([#1605]) ([**@lirem101**], [**@burgholzer**]) +- ✨ Add a `merge-single-qubit-rotation-gates` pass for merging consecutive + fixed and parameterized single-qubit gates using quaternions with global-phase + correction ([#1407], [#1674], [#2002], [#2038]) ([**@J4MMlE**], + [**@denialhaag**], [**@MatthiasReumann**], [**@simon1hofmann**]) +- ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], + [#1676], [#1706], [#1776], [#1836], [#1934], [#2000], [#2018], [#2105]) + ([**@denialhaag**], [**@burgholzer**]) +- ✨ Add a `place-and-route` pass for mapping scalar- and tensor-allocated + circuits to compiler-target topologies while preserving target site IDs and + materializing routing workspace on demand ([#1537], [#1547], [#1568], [#1581], + [#1583], [#1588], [#1600], [#1664], [#1709], [#1716], [#1748], [#1805], + [#1870], [#1904], [#1911], [#1951], [#1997], [#2016], [#2060]) + ([**@MatthiasReumann**], [**@burgholzer**]) +- ✨ Add a pass for qubit reuse in quantum programs, as well as related + auxiliary passes and patterns ([#1705], [#1755], [#1756], [#1923], [#1924], + [#2039]) ([**@DRovara**], [**@burgholzer**], [**@simon1hofmann**]) +- ✨ Add initial infrastructure for new QC and QCO MLIR dialects ([#1264], + [#1330], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], + [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], + [#1513], [#1521], [#1542], [#1548], [#1550], [#1554], [#1567], [#1569], + [#1570], [#1572], [#1573], [#1580], [#1602], [#1603], [#1620], [#1623], + [#1626], [#1627], [#1635], [#1638], [#1673], [#1675], [#1700], [#1717], + [#1728], [#1730], [#1749], [#1751], [#1762], [#1765], [#1780], [#1781], + [#1782], [#1806], [#1807], [#1815], [#1808], [#1824], [#1869], [#1872], + [#1886], [#1914], [#1925], [#1927], [#1935], [#1936], [#1938], [#1975], + [#1976], [#2006], [#2014], [#2015], [#2017], [#2026], [#2028], [#2058], + [#2125]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], + [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], + [**@simon1hofmann**], [**@J4MMlE**]) ### Changed @@ -162,12 +211,25 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#390)._ - 💥 Remove the unused `pybind11` CMake helper and rename `add_mqt_python_binding_nanobind` to `add_mqt_python_binding` ([#2106]) ([**@denialhaag**]) +- 💥 Replace the MQT-specific QDMI primitive `options` mappings with explicit + shot and precision defaults ([#2084]) ([**@burgholzer**]) - 💥 Move Python QDMI entities and the neutral-atom specialization to QDMI namespaces, expose device registration and opening through `mqt.core.qdmi.driver`, retain v3 FoMaC compatibility aliases, and let the Qiskit adapter open stable device IDs directly ([#2074]) ([**@burgholzer**]) -- 🚀 Reduce ZX diagram growth for multi-controlled X gates with an exact - ancilla-free quadratic decomposition ([#1984]) ([**@burgholzer**]) +- ⬆️ Update `nanobind` to version 2.14.0 ([#2073]) ([**@denialhaag**]) +- 🛡️ Isolate DDSIM QIR job execution and restrict statevector extraction to + terminal `irreversible` regions of Base-profile programs ([#2036]) + ([**@burgholzer**]) +- 💥 Update the QIR runner for QIR 2.1 entry points and resource management, + with entry-point selection and reproducible multi-shot execution ([#2035]) + ([**@burgholzer**]) +- 💥 Require LLVM/MLIR and QIR support in every MQT Core build and remove the + corresponding build options ([#1953]) ([**@burgholzer**]) +- ⬆️ Require LLVM 22.1 for C++ library builds ([#1549]) ([**@burgholzer**], + [**@denialhaag**]) +- 📦 Build MLIR by default for C++ library builds ([#1356]) ([**@burgholzer**], + [**@denialhaag**]) ### Fixed @@ -179,10 +241,41 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#390)._ ### Removed - 💥 Remove batch job submission from the QDMI client. `Device::submitJob` now - states that MQT Core does not support batch jobs ([#2148]) ([**@marcelwa**], - [**@burgholzer**]) + states that MQT Core does not support batch jobs ([#2148]) ([**@marcelwa**]) +- 💥 Remove the IQM JSON converter `qiskit_to_iqm_json` and the `MoveGate` from + the Qiskit plugin, which [QDMI-on-IQM] now owns ([#2114]) ([**@marcelwa**]) +- 💥 Remove the unused decision-diagram approximation algorithm, including the + `dd/Approximation.hpp` header, `dd::ApproximationMetadata`, and + `dd::approximate`. No replacement is provided ([#2154]) ([**@burgholzer**]) +- 💥 Remove `nlohmann_json` from the public package contract. MQT Core no longer + installs or exports the library, no installed header exposes a `nlohmann` + type, and the decision-diagram statistics report through strings and streams + ([#2138]) ([**@denialhaag**]) +- 💥 Remove the neutral-atom stack, which moves to MQT QMAP. This drops the + neutral-atom computation model, the neutral-atom FoMaC device session, the + neutral-atom QDMI device and its configuration, the `mqt.core.na` Python + module, `AodOperation`, and the `Move`, `Bridge`, `AodActivate`, + `AodDeactivate`, and `AodMove` operation kinds ([#2137]) ([**@denialhaag**]) +- 💥 Remove the random-number generator, seed, and `getGenerator()` method from + `QuantumComputation`; randomized algorithms now own generators initialized + from their seed arguments ([#2111]) ([**@simon1hofmann**]) - 💥 Remove QDMI device configuration through `[tool.qdmi]` in `pyproject.toml` and the vendored toml++ header ([#2116]) ([**@denialhaag**]) +- 💥 Remove the FoMaC compatibility name from the C++ and Python QDMI APIs. Use + the `qdmi` C++ namespace, headers, libraries, and CMake targets; the + `mqt.core.qdmi` and `mqt.core.na.qdmi` Python modules; and the module-level + Python driver functions ([#2115]) ([**@burgholzer**]) +- 💥 Remove the legacy `QuantumComputation`-to-MLIR translator and its C++ and + Python compiler inputs. Use OpenQASM, Qiskit circuits, or typed MLIR programs + as compiler inputs ([#2054]) ([**@burgholzer**]) +- 💥 Remove the ZX-calculus library, including the `mqt-core-zx` target, + `MQT::CoreZX` alias, `zx` headers and namespace, and its Boost.Multiprecision + and GMP build support. Equivalence-checking users should use [MQT QCEC]; its + ZX implementation is internal and does not provide a replacement public API. +- 🔥 Remove the density matrix support from the MQT Core DD package ([#1466]) + ([**@burgholzer**]) +- 🔥 Remove `datastructures` (`ds`) (sub)library from MQT Core ([#1458]) + ([**@burgholzer**]) ## [3.8.0] - 2026-07-30 @@ -764,8 +857,7 @@ for previous changelogs._ -[unreleased]: https://github.com/munich-quantum-toolkit/core/compare/v3.9.0...HEAD -[3.9.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.9.0 +[unreleased]: https://github.com/munich-quantum-toolkit/core/compare/v3.8.0...HEAD [3.8.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.8.0 [3.7.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.7.0 [3.6.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.6.1 @@ -811,20 +903,18 @@ for previous changelogs._ [#2116]: https://github.com/munich-quantum-toolkit/core/pull/2116 [#2115]: https://github.com/munich-quantum-toolkit/core/pull/2115 [#2114]: https://github.com/munich-quantum-toolkit/core/pull/2114 -[#2112]: https://github.com/munich-quantum-toolkit/core/pull/2112 [#2111]: https://github.com/munich-quantum-toolkit/core/pull/2111 [#2108]: https://github.com/munich-quantum-toolkit/core/pull/2108 [#2106]: https://github.com/munich-quantum-toolkit/core/pull/2106 [#2105]: https://github.com/munich-quantum-toolkit/core/pull/2105 [#2084]: https://github.com/munich-quantum-toolkit/core/pull/2084 -[#2082]: https://github.com/munich-quantum-toolkit/core/pull/2082 [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 +[#2073]: https://github.com/munich-quantum-toolkit/core/pull/2073 [#2066]: https://github.com/munich-quantum-toolkit/core/pull/2066 [#2060]: https://github.com/munich-quantum-toolkit/core/pull/2060 [#2058]: https://github.com/munich-quantum-toolkit/core/pull/2058 [#2054]: https://github.com/munich-quantum-toolkit/core/pull/2054 [#2049]: https://github.com/munich-quantum-toolkit/core/pull/2049 -[#2046]: https://github.com/munich-quantum-toolkit/core/pull/2046 [#2043]: https://github.com/munich-quantum-toolkit/core/pull/2043 [#2042]: https://github.com/munich-quantum-toolkit/core/pull/2042 [#2039]: https://github.com/munich-quantum-toolkit/core/pull/2039 @@ -859,17 +949,14 @@ for previous changelogs._ [#1994]: https://github.com/munich-quantum-toolkit/core/pull/1994 [#1993]: https://github.com/munich-quantum-toolkit/core/pull/1993 [#1992]: https://github.com/munich-quantum-toolkit/core/pull/1992 -[#1989]: https://github.com/munich-quantum-toolkit/core/pull/1989 [#1987]: https://github.com/munich-quantum-toolkit/core/pull/1987 [#1986]: https://github.com/munich-quantum-toolkit/core/pull/1986 -[#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 [#1983]: https://github.com/munich-quantum-toolkit/core/pull/1983 [#1980]: https://github.com/munich-quantum-toolkit/core/pull/1980 [#1979]: https://github.com/munich-quantum-toolkit/core/pull/1979 [#1978]: https://github.com/munich-quantum-toolkit/core/pull/1978 [#1976]: https://github.com/munich-quantum-toolkit/core/pull/1976 [#1975]: https://github.com/munich-quantum-toolkit/core/pull/1975 -[#1974]: https://github.com/munich-quantum-toolkit/core/pull/1974 [#1973]: https://github.com/munich-quantum-toolkit/core/pull/1973 [#1972]: https://github.com/munich-quantum-toolkit/core/pull/1972 [#1967]: https://github.com/munich-quantum-toolkit/core/pull/1967 diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h new file mode 100644 index 0000000000..4e4db76275 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h @@ -0,0 +1,243 @@ +/* + * 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/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/Drivers.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** + * This file provides information of available arith operations. It calculates + * the result of valid arith operations. Operations are only valid with one to + * two operands, not if they are applied to sequences. + */ +inline int64_t getArithIntegerOpResult(mlir::Operation* operation, + const int64_t value1, + const int64_t value2 = 0, + const int64_t value3 = 0) { + + for (auto operand : operation->getOperands()) { + if (mlir::isa(operand.getType())) { + llvm::report_fatal_error( + "Constant propagation does not support vectors as classical types."); + } + } + + const auto intTy = + dyn_cast(operation->getResult(0).getType()); + if (!intTy) { + llvm::report_fatal_error( + "IntegerType is needed to apply arith::integer operation."); + } + + const unsigned width = intTy.getWidth(); + if (width > 64) { + llvm::report_fatal_error( + "Result of an arith operation cannot be safely stored in a 64-bit " + "integer, which is required by constant propagation."); + } + + // APInt respects the signedness of the underlying MLIR type. + const bool isSigned = intTy.isSigned(); + const llvm::APInt a(width, static_cast(value1), isSigned); + const llvm::APInt b(width, static_cast(value2), isSigned); + const llvm::APInt c(width, static_cast(value3), isSigned); + + return mlir::TypeSwitch(operation) + .Case([&](auto) { return (a + b).getSExtValue(); }) + .Case([&](auto) { return (a & b).getSExtValue(); }) + .Case([&](mlir::arith::CmpIOp cmp) { + using Pred = mlir::arith::CmpIPredicate; + bool result = false; + switch (cmp.getPredicate()) { + case Pred::eq: + result = (a == b); + break; + case Pred::ne: + result = (a != b); + break; + case Pred::slt: + result = a.slt(b); + break; + case Pred::sle: + result = a.sle(b); + break; + case Pred::sgt: + result = a.sgt(b); + break; + case Pred::sge: + result = a.sge(b); + break; + case Pred::ult: + result = a.ult(b); + break; + case Pred::ule: + result = a.ule(b); + break; + case Pred::ugt: + result = a.ugt(b); + break; + case Pred::uge: + result = a.uge(b); + break; + } + + return result ? int64_t{1} : int64_t{0}; + }) + .Case([&](auto) { + if (b.isZero()) { + llvm::report_fatal_error( + "Handling of division by zero in mlir::arith::DivSIOp is not " + "supported by constant propagation."); + } + if (b.isAllOnes() && a.isMinSignedValue()) { + llvm::report_fatal_error( + "Handling of INT_MIN / -1 in mlir::arith::DivSIOp is not " + "supported by constant propagation."); + } + return a.sdiv(b).getSExtValue(); + }) + .Case([&](auto) { + return a.getSExtValue() > b.getSExtValue() ? a.getSExtValue() + : b.getSExtValue(); + }) + .Case([&](auto) { + return a.getSExtValue() < b.getSExtValue() ? a.getSExtValue() + : b.getSExtValue(); + }) + .Case([&](auto) { return (a * b).getSExtValue(); }) + .Case([&](auto) { return (a | b).getSExtValue(); }) + .Case([&](auto) { return (a - b).getSExtValue(); }) + .Case([&](auto) { return (a ^ b).getSExtValue(); }) + .Case([&](auto) { + // SelectOp: first operand is the i1 condition. + // In our helper `value1` is the condition (0 == false). + return (a != llvm::APInt(width, 0, true)) ? b.getSExtValue() + : c.getSExtValue(); + }) + .Default([](auto*) { + llvm::report_fatal_error("Unsupported integer operation in " + "mlir::qco::classicalarithoperation"); + return 0; + }); +} + +inline double getArithDoubleOpResult(mlir::Operation* operation, + const double value1, + const double value2 = 0.0) { + for (mlir::Value operand : operation->getOperands()) { + if (mlir::isa(operand.getType())) { + llvm::report_fatal_error( + "Constant propagation does not support vectors as classical types."); + } + } + + const auto floatTy = + dyn_cast(operation->getResult(0).getType()); + if (!floatTy) { + llvm::report_fatal_error("Expected floating-point result type."); + } + + const llvm::fltSemantics& sem = floatTy.getFloatSemantics(); + constexpr auto rm = llvm::APFloat::rmNearestTiesToEven; + bool losesInfo = false; + + llvm::APFloat lhs(value1); + llvm::APFloat rhs(value2); + lhs.convert(sem, rm, &losesInfo); + if (losesInfo) { + llvm::report_fatal_error("value1 cannot be represented safely."); + } + + losesInfo = false; + rhs.convert(sem, rm, &losesInfo); + if (losesInfo) { + llvm::report_fatal_error("value2 cannot be represented safely."); + } + + llvm::APFloat result = lhs; + + const bool supported = + mlir::TypeSwitch(operation) + .Case([&](auto) { + result = lhs; + auto status = result.add(rhs, rm); + return (status & llvm::APFloat::opInvalidOp) == 0; + }) + .Case([&](auto) { + result = lhs; + auto status = result.divide(rhs, rm); + return (status & llvm::APFloat::opInvalidOp) == 0; + }) + .Case([&](auto) { + result = llvm::maximum(lhs, rhs); + return true; + }) + .Case([&](auto) { + result = llvm::maxnum(lhs, rhs); + return true; + }) + .Case([&](auto) { + result = llvm::minimum(lhs, rhs); + return true; + }) + .Case([&](auto) { + result = llvm::minnum(lhs, rhs); + return true; + }) + .Case([&](auto) { + result = lhs; + auto status = result.multiply(rhs, rm); + return (status & llvm::APFloat::opInvalidOp) == 0; + }) + .Case([&](auto) { + result = lhs; + result.changeSign(); + return true; + }) + .Case([&](auto) { + result = lhs; + const auto status = result.mod(rhs); + return (status & llvm::APFloat::opInvalidOp) == 0; + }) + .Case([&](auto) { + result = lhs; + return result.subtract(rhs, rm) == llvm::APFloat::opOK; + }) + .Default([](auto) { return false; }); + + if (!supported) { + llvm::report_fatal_error("Unsupported floating-point operation in " + "mlir::qco::classicalarithoperation"); + } + + bool loseInfoResult = false; + result.convert(llvm::APFloat::IEEEdouble(), rm, &loseInfoResult); + if (loseInfoResult) { + llvm::report_fatal_error( + "Floating-point fold result cannot be represented safely as double."); + } + const double folded = result.convertToDouble(); + + return folded; +} diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h new file mode 100644 index 0000000000..88360cd29a --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h @@ -0,0 +1,228 @@ +/* + * 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/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/Drivers.h" + +#include + +#include +#include +#include +#include +#include + +/** + * This file provides information of available quantum gates as mappings. It is + * used in constant propagation to get the factors each amplitude has to be + * multiplied with to get the new amplitudes after a gate application. + */ + +namespace mlir::qco { +using Complex = std::complex; + +using ResultMap = + std::unordered_map>; + +constexpr double inv_sqrt2 = 1.0 / std::numbers::sqrt2; + +inline std::unordered_map> +getQubitMappingOfGates(mlir::Operation* gate, const std::span& params) { + + return mlir::TypeSwitch< + mlir::Operation*, + std::unordered_map>>( + gate) + .Case([&](auto) { + return ResultMap{{0, {{0, Complex(1, 0)}}}, {1, {{1, Complex(1, 0)}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{0, Complex(inv_sqrt2, 0)}, {1, Complex(inv_sqrt2, 0)}}}, + {1, {{0, Complex(inv_sqrt2, 0)}, {1, Complex(-inv_sqrt2, 0)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{1, Complex(1, 0)}}}, {1, {{0, Complex(1, 0)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{1, Complex(0, 1)}}}, {1, {{0, Complex(0, -1)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{0, Complex(1, 0)}}}, {1, {{1, Complex(-1, 0)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{0, Complex(1, 0)}}}, {1, {{1, Complex(0, 1)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{0, Complex(1, 0)}}}, {1, {{1, Complex(0, -1)}}}}}; + }) + .Case([&](auto) { + return ResultMap{{{0, {{0, Complex(1, 0)}}}, + {1, {{1, Complex(inv_sqrt2, inv_sqrt2)}}}}}; + }) + .Case([&](auto) { + return ResultMap{{{0, {{0, Complex(1, 0)}}}, + {1, {{1, Complex(inv_sqrt2, -inv_sqrt2)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{0, Complex(0.5, 0.5)}, {1, Complex(0.5, -0.5)}}}, + {1, {{0, Complex(0.5, -0.5)}, {1, Complex(0.5, 0.5)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{0, Complex(0.5, -0.5)}, {1, Complex(0.5, 0.5)}}}, + {1, {{0, Complex(0.5, 0.5)}, {1, Complex(0.5, -0.5)}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{{{0, {{0, Complex(c, 0)}, {1, Complex(0, -s)}}}, + {1, {{0, Complex(0, -s)}, {1, Complex(c, 0)}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{{{0, {{0, Complex(c, 0)}, {1, Complex(s, 0)}}}, + {1, {{0, Complex(-s, 0)}, {1, Complex(c, 0)}}}}}; + }) + .Case([&](auto) { + const double halfParameter = 0.5 * params[0]; + return ResultMap{{{0, {{0, exp(Complex(0, -halfParameter))}}}, + {1, {{1, exp(Complex(0, halfParameter))}}}}}; + }) + .Case([&](auto) { + return ResultMap{{{0, {{0, Complex(1, 0)}}}, + {1, {{1, exp(Complex(0, params[0]))}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{{{0, + {{0, Complex(c, 0)}, + {1, exp(Complex(0, params[1])) * Complex(0, -s)}}}, + {1, + {{0, exp(Complex(0, -params[1])) * Complex(0, -s)}, + {1, Complex(c, 0)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, + {{0, Complex(inv_sqrt2, 0)}, + {1, inv_sqrt2 * exp(Complex(0, params[0]))}}}, + {1, + {{0, -inv_sqrt2 * exp(Complex(0, params[1]))}, + {1, inv_sqrt2 * exp(Complex(0, params[0] + params[1]))}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{ + {{0, {{0, Complex(c, 0)}, {1, exp(Complex(0, params[1])) * s}}}, + {1, + {{0, -exp(Complex(0, params[2])) * s}, + {1, exp(Complex(0, params[1] + params[2])) * c}}}}}; + }) + .Case([&](auto) { + return ResultMap{{{0, {{0, Complex(1, 0)}}}, + {1, {{2, Complex(1, 0)}}}, + {2, {{1, Complex(1, 0)}}}, + {3, {{3, Complex(1, 0)}}}}}; + }) + .Case([&](auto) { + return ResultMap{{{0, {{0, Complex(1, 0)}}}, + {1, {{2, Complex(0, 1)}}}, + {2, {{1, Complex(0, 1)}}}, + {3, {{3, Complex(1, 0)}}}}}; + }) + .Case([&](auto) { + return ResultMap{{{0, {{0, Complex(1, 0)}}}, + {1, {{2, Complex(1, 0)}}}, + {2, {{3, Complex(1, 0)}}}, + {3, {{1, Complex(1, 0)}}}}}; + }) + .Case([&](auto) { + return ResultMap{ + {{0, {{2, Complex(inv_sqrt2, 0)}, {3, Complex(0, -inv_sqrt2)}}}, + {1, {{2, Complex(0, -inv_sqrt2)}, {3, Complex(inv_sqrt2, 0)}}}, + {2, {{0, Complex(inv_sqrt2, 0)}, {1, Complex(0, inv_sqrt2)}}}, + {3, {{0, Complex(0, inv_sqrt2)}, {1, Complex(inv_sqrt2, 0)}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{{{0, {{0, Complex(c, 0)}, {3, Complex(0, -s)}}}, + {1, {{1, Complex(c, 0)}, {2, Complex(0, -s)}}}, + {2, {{1, Complex(0, -s)}, {2, Complex(c, 0)}}}, + {3, {{0, Complex(0, -s)}, {3, Complex(c, 0)}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{{{0, {{0, Complex(c, 0)}, {3, Complex(0, s)}}}, + {1, {{1, Complex(c, 0)}, {2, Complex(0, -s)}}}, + {2, {{1, Complex(0, -s)}, {2, Complex(c, 0)}}}, + {3, {{0, Complex(0, s)}, {3, Complex(c, 0)}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{{{0, {{0, Complex(c, 0)}, {2, Complex(0, -s)}}}, + {1, {{1, Complex(c, 0)}, {3, Complex(0, s)}}}, + {2, {{0, Complex(0, -s)}, {2, Complex(c, 0)}}}, + {3, {{1, Complex(0, s)}, {3, Complex(c, 0)}}}}}; + }) + .Case([&](auto) { + const double halfParam = 0.5 * params[0]; + Complex ePos = exp(Complex(0, halfParam)); + Complex eNeg = exp(Complex(0, -halfParam)); + return ResultMap{{{0, {{0, eNeg}}}, + {1, {{1, ePos}}}, + {2, {{2, ePos}}}, + {3, {{3, eNeg}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{{{0, {{0, Complex(1, 0)}}}, + {1, + {{1, Complex(c, 0)}, + {2, Complex(0, -s) * exp(Complex(0, params[1]))}}}, + {2, + {{1, Complex(0, -s) * exp(Complex(0, -params[1]))}, + {2, Complex(c, 0)}}}, + {3, {{3, Complex(1, 0)}}}}}; + }) + .Case([&](auto) { + const double c = cos(0.5 * params[0]); + const double s = sin(0.5 * params[0]); + return ResultMap{{{0, + {{0, Complex(c, 0)}, + {3, Complex(0, -s) * exp(Complex(0, params[1]))}}}, + {1, {{1, Complex(1, 0)}}}, + {2, {{2, Complex(1, 0)}}}, + {3, + {{0, Complex(0, -s) * exp(Complex(0, -params[1]))}, + {3, Complex(c, 0)}}}}}; + }) + .Default([&](auto) -> ResultMap { + llvm::report_fatal_error("Unsupported gate in mlir::qco::gatetomap"); + }); +} +} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp new file mode 100644 index 0000000000..28d0466c1a --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp @@ -0,0 +1,318 @@ +/* + * 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 "QuantumState.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { +/** + * @brief This class represents a hybrid state. + * + * This class holds a QuantumState and a mapping form values to integers or + * doubles, as well as a probability. + */ +class HybridState { + bool top = false; + std::shared_ptr qState; + double probability; + llvm::DenseMap integerValues; + llvm::DenseMap doubleValues; + + HybridState() : probability(0.0) {} + + /** + * @brief Checks if all positive classical controls hold and all negative + * classical controls do not hold. Aborts if a classical control value cannot + * be found. + * + * @param posCtrlsClassical An array of the classical positive control values. + * @param negCtrlsClassical An array of the classical negative control values. + * @return True if the controls together evaluate to true. + */ + bool isOperationExecutable(const std::span posCtrlsClassical, + const std::span negCtrlsClassical) { + for (const Value posCtrl : posCtrlsClassical) { + if (integerValues.contains(posCtrl) && integerValues.at(posCtrl) == 0) { + return false; + } + if (doubleValues.contains(posCtrl) && + std::norm(doubleValues.at(posCtrl)) < 1e-4) { + return false; + } + if (!doubleValues.contains(posCtrl) && !integerValues.contains(posCtrl)) { + llvm::report_fatal_error( + "HybridState needs a classical value for operation control that is " + "not existent in current HybridState."); + } + } + for (const Value negCtrl : negCtrlsClassical) { + if (integerValues.contains(negCtrl) && integerValues.at(negCtrl) != 0) { + return false; + } + if (doubleValues.contains(negCtrl) && + std::norm(doubleValues.at(negCtrl)) >= 1e-4) { + return false; + } + if (!doubleValues.contains(negCtrl) && !integerValues.contains(negCtrl)) { + llvm::report_fatal_error( + "HybridState needs a classical value for operation control that is " + "not existent in current HybridState."); + } + } + return true; + } + + /** + * @brief This method applies a measurement or reset. + * + * This method applies a measurement or reset, changing the qubits and the + * classical values (if a measurement is applied) corresponding to the + * measurement. Aborts if a classical control value cannot be found. + * + * @param quantumTarget The index of the qubit to be measured. + * @param reset True if a reset is applied. + * @param classicalTarget The value to save the measurement result to. + * @param posCtrlsClassical An array of the classical positive control values. + * @param negCtrlsClassical An array of the classical negative control values. + * @return One or two hybrid states corresponding to the measurement or reset + * outcomes. + */ + std::vector + propagateMeasurementOrReset(const unsigned int quantumTarget, + const bool reset, + const Value classicalTarget = nullptr, + const std::span posCtrlsClassical = {}, + const std::span negCtrlsClassical = {}) { + if (top || !isOperationExecutable(posCtrlsClassical, negCtrlsClassical)) { + return {*this}; + } + + std::vector results; + + const auto [newQuantumStates, availableStates] = + reset ? qState->resetQubit(quantumTarget) + : qState->measureQubit(quantumTarget); + + for (const int64_t i : {0, 1}) { + if (!availableStates.contains(i) || !availableStates.at(i)) { + continue; + } + + const auto& [measProbability, measQS] = newQuantumStates.at(i); + auto newHybrid = HybridState(); + newHybrid.probability = measProbability * probability; + newHybrid.integerValues = integerValues; + newHybrid.doubleValues = doubleValues; + if (!reset) { + newHybrid.integerValues[classicalTarget] = i; + } + newHybrid.qState = measQS; + + results.push_back(newHybrid); + } + + return results; + } + +public: + explicit HybridState(std::span globalQubitNumber, + std::size_t maxNonzeroAmplitudes, + double probability = 1.0); + + ~HybridState(); + + void print(std::ostream& os) const; + + [[nodiscard("HybridState::toString called but ignored")]] + std::string toString() const; + + [[nodiscard("HybridState::isHybridStateTop called but ignored")]] bool + isHybridStateTop() const { + return top; + } + + /** + * @brief This method adds a classical integer value to the hybrid state. + * + * @param value The value object of the new value. + * @param number The number of the new value. + */ + void addIntegerValue(Value value, int64_t number); + + /** + * @brief This method adds a classical double value to the hybrid state. + * + * @param value The value object of the new value. + * @param number The number of the new value. + */ + void addDoubleValue(Value value, double number); + + /** + * @brief This method changes the global index of a qubit in the quantum + * state. + * + * @param target The old global index of a qubit. + * @param newIndex The new global index for the qubit. + */ + void changeGlobalIndex(unsigned int target, unsigned int newIndex) const; + + /** + * @brief This method applies a gate to the state. + * + * This method changes the hybrid state according to a gate. Aborts if a + * classical control value cannot be found. + * + * @param gate The name of the gate to be applied. + * @param targets An array of the indices of the target qubits. + * @param ctrlsQuantum An array of the global indices of the ctrl qubits. + * @param posCtrlsClassical An array of the classical positive control values. + * @param negCtrlsClassical An array of the classical negative control values. + * @param params The values of parameters applied to the gate. + */ + void propagateGate(Operation* gate, std::span targets, + std::span ctrlsQuantum = {}, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}, + std::span params = {}); + + /** + * @brief This method applies a measurement. + * + * This method applies a measurement, changing the qubits and the classical + * values corresponding to the measurement. Aborts if a classical control + * value cannot be found. + * + * @param quantumTarget The index of the qubit to be measured. + * @param classicalTarget The value to save the measurement result to. + * @param posCtrlsClassical An array of the classical positive control values. + * @param negCtrlsClassical An array of the classical negative control values. + * @return One or two hybrid states corresponding to the measurement + * outcomes. + */ + std::vector + propagateMeasurement(unsigned int quantumTarget, Value classicalTarget, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}); + + /** + * @brief This method applies a reset. + * + * This method applies a reset, changing the qubits and creates one or two new + * states. The procedure is done as if the qubit was measured, put to zero if + * the measurement was one, and the result discarded. Aborts if a classical + * control value cannot be found. + * + * @param target The index of the qubit to be measured. + * @param posCtrlsClassical An array of the classical positive control values. + * @param negCtrlsClassical An array of the classical negative control values. + * @return One or two hybrid states corresponding to the measurement outcomes + * during the reset, but with the qubit always in the zero state. + */ + std::vector + propagateReset(unsigned int target, std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}); + + /** + * @brief This method applies a classical operation. + * + * This method changes the hybrid state according to a classical operation. + * The operation might be controlled by classical values. + * + * @param op The operation to be applied. + * @param dest The value the result of the operation is written to. + * @param operand1 The first value used by the operation. + * @param operand2 The second value used by the operation, might be null. + * @param operand3 The third value used by the operation, might be null. + * @param posCtrlsClassical An array of the classical positive control values. + * @param negCtrlsClassical An array of the classical negative control values. + */ + void propagateClassicalOperation(Operation* op, Value dest, Value operand1, + Value operand2 = nullptr, + Value operand3 = nullptr, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}); + + /** + * @brief This method unifies two HybridStates. + * + * This method unifies the current HybridState with the given one and returns + * a new HybridState, if the new state has no more than maxNonzeroAmplitudes. + * Otherwise, the state becomes top. + * + * @param that The HybridState to unify this with. + */ + HybridState unify(const HybridState& that); + + bool operator==(const HybridState& that) const; + + [[nodiscard("HybridState::isQubitAlwaysOne called but ignored")]] bool + isQubitAlwaysOne(unsigned int q) const; + + [[nodiscard("HybridState::isQubitAlwaysZero called but ignored")]] bool + isQubitAlwaysZero(unsigned int q) const; + + [[nodiscard("HybridState::isValueTrue called but ignored")]] bool + isValueTrue(Value v) const; + + /** + * @brief Checks if a given combination of values-qubit values has a nonzero + * probability. + * + * This method receives a number of qubit and values and checks whether + * they have for a given value always a zero amplitude. If the hybridState is + * top, it is not guaranteed that the amplitude is always zero and false is + * returned. + * The values for the classical values are not the numeric ones, but whether + * they are zero (false) or non-zero (true). Aborts if a classical value + * cannot be found. + * + * @param qubitValues Pairs of the qubits that are being checked and the + * values that they are being checked for. + * @param classicalValues The classical values to check. + * @returns True if the amplitude is always zero, false otherwise. + */ + [[nodiscard("HybridState::hasAlwaysZeroProbability called but ignored")]] bool + hasAlwaysZeroProbability( + const std::unordered_map& qubitValues, + const llvm::DenseMap& classicalValues) const; + + /** + * @brief Returns a classical value that is equivalent to qubit. + * + * Returns a classical value that is always true (=/= 0) when the given qubit + * is 1, and the boolean value true. Alternatively, it can return a value that + * is always false (== 0) if the qubit is 1. In that case, the returned bool + * is false. + * + * @param qubit Index of qubit. + * @returns A map of classical values that are equivalent or inverse to qubit. + * Th emaps value is true, if the qubit is equivalent to the value. False, if + * the qubit is the inverse of the value. + */ + [[nodiscard("HybridState::getValueThatIsEquivalentToQubit called but " + "ignored")]] llvm::DenseMap + getValueThatIsEquivalentToQubit(unsigned int qubit) const; +}; +} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp new file mode 100644 index 0000000000..d6f0d28d4e --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +/** + * @brief Result of a measurement or reset. + * + * The result contains 0, 1 or 2 QuantumStates, together with their respective + * results. + */ +struct MeasurementResult { + // The pair is (probability, resulting state). + std::unordered_map>> + states; + + // Which entries are available + std::unordered_map availableStates; +}; + +/** + * @brief This class represents a quantum state. + * + * This class holds n qubits in different basis states with their corresponding + * complex amplitude. It holds the information about which global qubit number + * corresponds to which local qubit number. + */ +class QuantumState { + std::size_t nQubits; + std::size_t maxNonzeroAmplitudes; + std::unordered_map globalToLocalQubitNumber; + std::unordered_map> amplitudeMap; + bool top; + + std::string qubitStringToBinary(const unsigned int q) const { + std::string result; + result.reserve(nQubits); + + for (std::size_t i = nQubits; i > 0; --i) { + result.push_back((q >> (i - 1) & 1U) != 0 ? '1' : '0'); + } + return result; + } + + /** @brief Puts a zero or one at bit position n in a bitstring. + * + * @param bitstring Bitstring that gets altered + * @param position Position of bit that needs to be changed + * @param one True if bit should be set to one + * @return The altered bitstring + */ + static unsigned int setBitN(const unsigned int bitstring, + const unsigned int position, const bool one) { + const unsigned int mask = 1U << position; + if (one) { + return bitstring | mask; + } + return bitstring & ~mask; + } + + /** + * @brief This method receives a gate mapping and a bitmask ctrls and the + * positions of the one (or two) qubit targets. + * + * This method receives a two qubit gate mapping and a bitmask for ctrl qubits + * and a span with the first target qubit position at first position and the + * potential second target qubit at second position. The gate is applied to + * the valid qubit states. It returns the map which would be the qubit state + * after gate application. + * + * @param gateMapping The mapping representing the gate + * @param positionOfTargetQubits The position of the qubit targets. + * @param bitmaskForCtrls The bitmask of the positively controlling qubits. + * @return The qubit state after the gate has been applied. + */ + std::unordered_map> + getNewMappingFromQubitGate( + const std::unordered_map< + unsigned int, std::unordered_map>>& + gateMapping, + const std::span positionOfTargetQubits, + const unsigned int bitmaskForCtrls) { + std::unordered_map> newValues; + const auto numberOfTargetValues = 1U << positionOfTargetQubits.size(); + + for (const auto& [key, value] : amplitudeMap) { + if ((bitmaskForCtrls & key) != bitmaskForCtrls) { + newValues[key] += value; + continue; + } + + unsigned int mapFrom = 0; + + std::vector keysForNewValue(numberOfTargetValues); + + // Find the target keys from the current keys + for (unsigned int i = 0; i < numberOfTargetValues; ++i) { + if (i % 2 == 1) { + keysForNewValue[i] = setBitN(key, positionOfTargetQubits[0], true); + } else { + keysForNewValue[i] = setBitN(key, positionOfTargetQubits[0], false); + } + if (numberOfTargetValues > 2) { + if (i > 1) { + keysForNewValue[i] = + setBitN(keysForNewValue[i], positionOfTargetQubits[1], true); + } else { + keysForNewValue[i] = + setBitN(keysForNewValue[i], positionOfTargetQubits[1], false); + } + } + if (keysForNewValue[i] == key) { + mapFrom = i; + } + } + + auto outerIt = gateMapping.find(mapFrom); + if (outerIt != gateMapping.end()) { + const auto& mapForThisQubit = outerIt->second; + for (unsigned int i = 0; i < numberOfTargetValues; i++) { + auto innerIt = mapForThisQubit.find(i); + if (innerIt == mapForThisQubit.end()) { + continue; + } + if (auto valueToI = innerIt->second; abs(valueToI) > 1e-4) { + newValues[keysForNewValue.at(i)] += valueToI * value; + } + } + } + } + + return newValues; + } + + /** + * @brief This method applies a measurement or reset to the qubits. + * + * This method applies a measurement or reset to the qubits. It returns the + * QuantumState in case the measurement was 0 and in case it was 1, alongside + * the respective probabilities. If a reset was applied, the qubit that was + * measured is set to 0. + * + * @param target The global index of the qubit to be measured. + * @param reset True if target should be reset in addition to measured. + * @return MeasurementResult, containing the probability for the result and + * the QuantumStates after measurement. + */ + MeasurementResult measureOrResetQubit(const unsigned int target, + const bool reset) { + const auto qubitMask = 1U << globalToLocalQubitNumber.at(target); + + double probabilityZero = 0.0; + double probabilityOne = 0.0; + std::unordered_map> newValuesZeroRes; + std::unordered_map> newValuesOneRes; + + for (const auto& [key, value] : amplitudeMap) { + unsigned int newKey = key; + const bool isZero = (qubitMask & key) == 0; + const double probability = norm(value); + if (isZero) { + probabilityZero += probability; + newValuesZeroRes[newKey] = value; + } else { + if (reset) { + newKey = key ^ qubitMask; + } + probabilityOne += probability; + newValuesOneRes[newKey] = value; + } + } + + auto globalKeysView = std::views::keys(globalToLocalQubitNumber); + std::vector globalKeys{globalKeysView.begin(), + globalKeysView.end()}; + MeasurementResult res = {}; + if (probabilityZero > 1e-4) { + auto stateZero = + std::make_shared(globalKeys, maxNonzeroAmplitudes); + stateZero->amplitudeMap = std::move(newValuesZeroRes); + stateZero->normalize(); + res.states[0] = {probabilityZero, stateZero}; + res.availableStates[0] = true; + } + if (probabilityOne > 1e-4) { + auto stateOne = + std::make_shared(globalKeys, maxNonzeroAmplitudes); + stateOne->amplitudeMap = std::move(newValuesOneRes); + stateOne->normalize(); + res.states[1] = {probabilityOne, stateOne}; + res.availableStates[1] = true; + } + + return res; + } + +public: + QuantumState(std::span globalQubitNumber, + std::size_t maxNonzeroAmplitudes); + + ~QuantumState() = default; + + void print(std::ostream& os) const; + + [[nodiscard("QuantumState::toString called but ignored")]] std::string + toString() const; + + [[nodiscard("QuantumState::== called but ignored")]] bool + operator==(const QuantumState& that) const; + + /** + * @brief This method normalizes the amplitudes of a state. + */ + void normalize(); + + /** + * @brief This method returns whether the quantum state is top. + * + * @return True if the quantum state is top. + */ + [[nodiscard("QuantumState::isTop called but ignored")]] bool isTop() const; + + /** + * @brief This method unifies two QuantumState. + * + * This method unifies the current QuantumState with the given one and returns + * a new QuantumState, if the new state has no more than maxNonzeroAmplitude + * nonzero amplitudes. Otherwise, QuantumState becomes top. + * + * @param that The QuantumState to unify this with. + */ + [[nodiscard("QuantumState::unify called but ignored")]] QuantumState + unify(const QuantumState& that); + + /** + * @brief This method changes the global index of a qubit. + * + * @param target The old global index of a qubit. + * @param newIndex The new global index for the qubit. + */ + void changeGlobalIndex(unsigned int target, unsigned int newIndex); + + /** + * @brief This method applies a global phase gate to the qubits. + * + * This method changes the amplitudes of a QuantumState according to the + * applied global phase, that can become a relative phase if it is controlled. + * + * @param ctrls A span of the global indices of the ctrl qubits. + * @param phase The phase of the gate. + */ + void propagateGPhaseGate(std::span ctrls, double phase); + + /** + * @brief This method applies a gate to the qubits. + * + * This method changes the amplitudes of a QuantumState according to the + * applied gate. Returns the current QuantumState if it has no more than + * maxNonZeroAmplitude nonzero amplitudes. Otherwise, QuantumState becomes + * top. + * + * @param gate The gate to be applied. + * @param targets A span of the global indices of the target qubits. + * @param ctrls A span of the global indices of the ctrl qubits. + * @param params The parameter applied to the gate. + */ + void propagateGate(Operation* gate, std::span targets, + std::span ctrls = {}, + std::span params = {}); + + /** + * @brief This method applies a measurement to the qubits. + * + * This method applies a measurement to the qubits. It returns the + * QuantumState in case the measurement was 0 and in case it was 1, alongside + * the respective probabilities. + * + * @param target The global index of the qubit to be measured. + * @return MeasurementResult, containing the probability for the result and + * the QuantumStates after measurement. + */ + [[nodiscard( + "QuantumState::measureQubit called but ignored")]] MeasurementResult + measureQubit(unsigned int target); + + /** + * @brief This method resets a qubit. + * + * This method resets a qubit. This is done by assuming that a measurement is + * applied and measurements in the one-state are set to the zero state. In + * order to not get a mixed state, the method returns one to two + * QuantumStates, one in case the measurement was 0 and one in case it was 1, + * alongside the respective probabilities. In both cases, the target qubit + * will be zero (as a reset is performed). + * + * @param target The global index of the qubit to be measured. + * @return MeasurementResult, containing the probability for the result and + * the QuantumStates after measurement. + */ + [[nodiscard("QuantumState::resetQubit called but ignored")]] MeasurementResult + resetQubit(unsigned int target); + + /** + * @brief This method checks if only amplitudes with a given qubit = 1 are + * nonzero. + * + * @param q The global index of the qubit. + * @return A set of one to two QuantumStates and their corresponding + * probabilities. + */ + [[nodiscard("QuantumState::isQubitAlwaysOne called but ignored")]] bool + isQubitAlwaysOne(unsigned int q) const; + + /** + * @brief This method checks if only amplitudes with a given qubit = 0 are + * nonzero. + * + * @param q The global index of the qubit. + * @return A set of one to two QuantumStates and their corresponding + * probabilities. + */ + [[nodiscard("QuantumState::isQubitAlwaysZero called but ignored")]] bool + isQubitAlwaysZero(unsigned int q) const; + + /** + * @brief Returns whether the given qubits have for a given value always a + * zero amplitude. + * + * This method receives a number of global qubit indices and checks whether + * they have for a given value always a zero amplitude. + * + * @param qubitValues Pairs of the qubits that are being checked and the + * values that they are being checked for. + * @returns True if the amplitude is always zero, false otherwise. + */ + [[nodiscard("QuantumState::hasAlwaysZeroAmplitude called but ignored")]] bool + hasAlwaysZeroAmplitude( + const std::unordered_map& qubitValues) const; +}; + +} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp new file mode 100644 index 0000000000..eff338d7d2 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp @@ -0,0 +1,544 @@ +/* + * 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 "HybridState.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +/** + * @brief Result of the check for superfluous values. + */ +struct SuperfluousResult { + bool completelySuperfluous = false; + llvm::DenseSet superfluousQubits; + llvm::DenseSet superfluousClassicalValues; +}; + +/** + * @brief Managing entries of the union table + */ +struct UnionTableEntry { + const unsigned int index; + bool top = false; + std::vector states; + // Values and global indices of the participating qubits + llvm::DenseSet participatingQubits = {}; + llvm::DenseSet participatingClassicalValues = {}; + + bool operator<(const UnionTableEntry& ute) const noexcept { + return index < ute.index; + } + + bool operator==(const UnionTableEntry& ute) const noexcept { + return index == ute.index; + } + + UnionTableEntry() : index(nextId()) {} + +private: + static unsigned int nextId() { + static std::atomic counter{0}; + return counter.fetch_add(1, std::memory_order_relaxed) + 1; + } +}; + +/** + * @brief This class represents a union table. + * + * This class holds multiple hybrid states and can propagate operations on the + * values in the states. + */ +class UnionTable { + bool allTop = false; + std::size_t maxNonzeroAmplitudes; + std::size_t maximumHybridEntries; + llvm::DenseMap> valuesToEntries = + llvm::DenseMap>(); + std::set> entries; + llvm::DenseMap qubitsToGlobalIndices; + + /** @brief: Collects a set of all participating entries. + * + * @param targets An array of the Values of the target qubits. + * @param ctrlsQuantum An array of the values of the ctrl qubits. + * @param posCtrlsClassical An array of the values of the ctrl bits. + * @param negCtrlsClassical An array of the values of the negative ctrl bits. + * @param params The parameter applied to the gate. + */ + std::set + collectParticipatingEntries(const std::span targets, + const std::span ctrlsQuantum, + const std::span posCtrlsClassical, + const std::span negCtrlsClassical, + const std::span params = {}) { + std::set participatingEntries; + for (auto const q : targets) { + participatingEntries.insert(*valuesToEntries.at(q)); + } + for (auto const q : ctrlsQuantum) { + participatingEntries.insert(*valuesToEntries.at(q)); + } + for (auto const i : posCtrlsClassical) { + participatingEntries.insert(*valuesToEntries.at(i)); + } + for (auto const i : negCtrlsClassical) { + participatingEntries.insert(*valuesToEntries.at(i)); + } + for (auto const i : params) { + participatingEntries.insert(*valuesToEntries.at(i)); + } + return participatingEntries; + } + + /** @brief Puts the given UnionTableEntries to top + * + * @param entriesToTop The UnionTableEntries to become top. + */ + void putEntriesToTop(const std::set& entriesToTop) { + auto topUnionTableEntry = UnionTableEntry(); + topUnionTableEntry.top = true; + for (const auto& e : entriesToTop) { + topUnionTableEntry.participatingQubits.insert( + e.participatingQubits.begin(), e.participatingQubits.end()); + topUnionTableEntry.participatingClassicalValues.insert( + e.participatingClassicalValues.begin(), + e.participatingClassicalValues.end()); + auto it = std::ranges::find_if( + entries, [&](auto const& entry) { return *entry == e; }); + if (it != entries.end()) { + entries.erase(it); + } + } + const auto ptrUte = std::make_shared(topUnionTableEntry); + entries.insert(ptrUte); + for (auto q : ptrUte->participatingQubits) { + valuesToEntries.erase(q); + valuesToEntries[q] = ptrUte; + } + for (auto c : ptrUte->participatingClassicalValues) { + valuesToEntries.erase(c); + valuesToEntries[c] = ptrUte; + } + } + + /** + * @brief This method unifies the given UnionTableEntries. + * + * This method unifies the given UnionTableEntries. If the new states have + * more than maxNonzeroAmplitudes, it handles these states as top. The same + * holds if the resulting hybridStates are more than maximumHybridEntries. + * + * @param entriesToUnify The UnionTableEntries to be unified. + * @return Whether the new entries are top or not. + */ + bool unifyEntries(const std::set& entriesToUnify) { + if (entriesToUnify.size() == 1) { + return entriesToUnify.begin()->top; + } + bool entriesBecomeTop = false; + for (const auto& e : entriesToUnify) { + entriesBecomeTop |= e.top; + } + + // Check if the number of entries would be too large + unsigned int numberOfNewEntries = 1; + for (const auto& e : entriesToUnify) { + if (e.top || + (!e.states.empty() && + numberOfNewEntries > maximumHybridEntries / e.states.size())) { + putEntriesToTop(entriesToUnify); + return true; + } + numberOfNewEntries *= e.states.size(); + } + + // Create new entry + auto newEntry = UnionTableEntry(); + newEntry.top = entriesBecomeTop; + for (const auto& e : entriesToUnify) { + auto classicalValues = e.participatingClassicalValues; + auto qubits = e.participatingQubits; + newEntry.participatingClassicalValues.insert(classicalValues.begin(), + classicalValues.end()); + newEntry.participatingQubits.insert(qubits.begin(), qubits.end()); + if (!newEntry.top && newEntry.states.empty()) { + newEntry.states = e.states; + continue; + } + if (newEntry.top || e.states.empty()) { + continue; + } + std::vector unifiedHS = {}; + for (auto hs1 : newEntry.states) { + for (const auto& hs2 : e.states) { + unifiedHS.push_back(hs1.unify(hs2)); + } + } + newEntry.states = unifiedHS; + } + + // Adapt global data structures to new entry + auto valuesToReplace = llvm::DenseSet(); + for (const auto& [v, _] : valuesToEntries) { + if (newEntry.participatingQubits.contains(v) || + newEntry.participatingClassicalValues.contains(v)) { + valuesToReplace.insert(v); + } + } + const auto ptrUTE = std::make_shared(newEntry); + for (const auto& v : valuesToReplace) { + valuesToEntries[v] = ptrUTE; + } + for (const auto& e : entriesToUnify) { + auto it = std::ranges::find_if( + entries, [&](auto const& entry) { return *entry == e; }); + if (it != entries.end()) { + entries.erase(it); + } + } + entries.insert(ptrUTE); + return entriesBecomeTop; + } + + /** + * @brief This method applies a swap gate by switching two qubits if the + * qubits are in different entries. + * + * @param targets The values that partake in the swap. + * @param newQuantumTargets The values after the swap. + */ + void applySwapGate(const std::span targets, + const std::span newQuantumTargets) { + for (const auto& hs : valuesToEntries.at(targets[0])->states) { + hs.changeGlobalIndex(qubitsToGlobalIndices.at(targets[0]), + qubitsToGlobalIndices.at(targets[1])); + } + for (const auto& hs : valuesToEntries.at(targets[1])->states) { + hs.changeGlobalIndex(qubitsToGlobalIndices.at(targets[1]), + qubitsToGlobalIndices.at(targets[0])); + } + const auto targetOneIndex = qubitsToGlobalIndices.at(targets[0]); + qubitsToGlobalIndices[targets[0]] = qubitsToGlobalIndices.at(targets[1]); + qubitsToGlobalIndices[targets[1]] = targetOneIndex; + std::vector reversedNewTargets(newQuantumTargets.begin(), + newQuantumTargets.end()); + std::ranges::reverse(reversedNewTargets); + replaceValuesGlobally(targets, reversedNewTargets); + } + + /** + * @brief This method returns classical values which are always either true or + * false. + * + * @return A map of values as keys. The values of th emap say whether the + * classical values are always true or always false. + */ + llvm::DenseMap + getClassicalValuesThatAreAlwaysTrueOrFalse() const { + llvm::DenseMap result; + for (auto const& e : entries) { + for (auto const& v : e->participatingClassicalValues) { + for (auto const& hs : e->states) { + const auto isTrue = hs.isValueTrue(v); + if (result.contains(v) && result.at(v) != isTrue) { + result.erase(v); + break; + } + result[v] = isTrue; + } + } + } + return result; + } + +public: + explicit UnionTable(std::size_t maxNonzeroAmplitudes, + std::size_t maximumHybridEntries); + + ~UnionTable(); + + void print(std::ostream& os) const; + + [[nodiscard("UnionTable::toString called but ignored")]] + std::string toString() const; + + /** @brief: Replaces values globally by new values. Aborts if the size of the + * two parameters is not equal. + * + * @param replacedValues Values to be replaced + * @param newValues Values the first values are replaced with. + */ + void replaceValuesGlobally(std::span replacedValues, + std::span newValues); + + [[nodiscard("UnionTable::allTop called but ignored")]] + bool areStatesAllTop(); + + /** + * @brief This method applies a gate to the qubits. + * + * This method changes the amplitudes of a QuantumState according to the + * applied gate. Aborts if a value is given but not found in the existing + * ones. + * + * @param gate The gate to be applied. + * @param targets An array of the Values of the target qubits. + * @param newQuantumTargets The value of the qubits after the gate. + * @param ctrlsQuantum An array of the values of the ctrl qubits. + * @param newCtrlsQuantum An values of the ctrl qubits after the gate. + * @param posCtrlsClassical An array of the values of the ctrl bits. + * @param negCtrlsClassical An array of the values of the negative ctrl bits. + * @param params The parameter applied to the gate. + */ + void propagateGate(Operation* gate, std::span targets, + std::span newQuantumTargets, + std::span ctrlsQuantum = {}, + std::span newCtrlsQuantum = {}, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}, + std::span params = {}); + + /** + * @brief This method propagates a classical operation. Aborts if a value is + * given, but not found in the existing ones. + * + * + * @param op The operation to be applied. + * @param targets An array of the Values of the target classical values. + * @param results The value of the result. + * @param posCtrlsClassical An array of the values of the ctrl bits. + * @param negCtrlsClassical An array of the values of the negative ctrl bits. + */ + void propagateClassicalOperation(Operation* op, std::span targets, + std::span results, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}); + + /** + * @brief This method applies a measurement. + * + * This method applies a measurement, changing the qubits and the classical + * bit corresponding to the measurement. Aborts if a value is given, but not + * found in the existing ones. This does not hold for the classical target, + * which can be newly created. + * + * @param quantumTarget The value of the qubit to be measured. + * @param newQuantumValue The value of the qubit after the measurement. + * @param classicalTarget The value of the bit to save the measurement result + * in. + * @param posCtrlsClassical An array of the values of the ctrl bits. + * @param negCtrlsClassical An array of the values of the negative ctrl bits. + */ + void propagateMeasurement(Value quantumTarget, Value newQuantumValue, + Value classicalTarget, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}); + + /** + * @brief This method propagates a qubit reset. + * + * This method propagates a qubit reset. This means that the qubit is put into + * zero state. It is also put in its own QubitState again if it does not + * correspond to already assigned bit values. Aborts if a value is given, but + * not found in the existing ones. + * + * @param quantumTarget The value of the qubit to be reset. + * @param newQuantumValue The value of the qubit after the reset. + * @param posCtrlsClassical An array of the values of the ctrl bits. + * @param negCtrlsClassical An array of the values of the negative ctrl bits. + */ + void propagateReset(Value quantumTarget, Value newQuantumValue, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}); + + /** + * @brief This method propagates a qubit alloc. + * + * This method propagates a qubit alloc. This means that the qubit is added to + * the UnionTable in zero state. + * + * @param qubit The value of the qubit to be allocated. + */ + void propagateQubitAlloc(Value qubit); + + /** + * @brief This method propagates an int alloc. + * + * This method propagates an int alloc. This means that the int is added to + * the UnionTable as new HybridState. + * + * @param intValue The value of the int to be allocated. + * @param number The number that the int is initialized with. + */ + void propagateIntAlloc(Value intValue, int64_t number); + + /** + * @brief This method propagates a double alloc. + * + * This method propagates a double alloc. This means that the double is added + * to the UnionTable as new HybridState. + * + * @param doubleValue The value of the double to be allocated. + * @param number The number that the double is initialized with. + */ + void propagateDoubleAlloc(Value doubleValue, double number); + + [[nodiscard("UnionTable::isQubitAlwaysOne called but ignored")]] bool + isQubitAlwaysOne(Value q) const; + + [[nodiscard("UnionTable::isQubitAlwaysZero called but ignored")]] bool + isQubitAlwaysZero(Value q) const; + + [[nodiscard( + "UnionTable::isClassicalValueAlwaysTrue called but ignored")]] bool + isClassicalValueAlwaysTrue(Value c) const; + + [[nodiscard( + "UnionTable::isClassicalValueAlwaysFalse called but ignored")]] bool + isClassicalValueAlwaysFalse(Value c) const; + + /** + * @brief Checks if a given combination of values-qubit values has a nonzero + * probability. + * + * This method receives a number of qubit and values and checks whether + * they have for a given value always a zero amplitude. + * The values for the classical values are not the numeric ones, but whether + * they are zero (false) or non-zero (true). Aborts if a value is given, but + * is not found in the existing ones. + * + * @param qubitValues Pairs of the qubits that are being checked and the + * values that they are being checked for. + * @param classicalValues The classical values to check. + * @returns True if the amplitude is always zero, false otherwise. + */ + [[nodiscard("HybridState::hasAlwaysZeroAmplitude called but ignored")]] bool + hasAlwaysZeroProbability( + const llvm::DenseMap& qubitValues, + const llvm::DenseMap& classicalValues) const; + + /** + * @brief Returns a classical value that is equivalent to qubit. + * + * Returns a classical value that is always true (=/= 0) when the given qubit + * is 1, and the boolean value true. Alternatively, it can return a value that + * is always false (== 0) if the qubit is 1. In that case, the returned bool + * is false. + * + * @param qubit Index of qubit. + * @returns A map of classical values that are equivalent or inverse to qubit. + * The value of the map is true, if the qubit is equivalent to the value. + * False, if the qubit is the inverse of the value. + */ + [[nodiscard( + "UnionTable::getValueThatIsEquivalentToQubit called but ignored")]] llvm:: + DenseMap + getValueThatIsEquivalentToQubit(Value qubit) const; + + /** + * @brief This method checks whether a diagonal gate only adds a global phase + * and returns it. + * + * This method receives a diagonal gate and checks, if only a global phase is + * added to the circuit by it under the current configuration. If that is the + * case, the returned optional contains the global phase. Only works with + * 1-qubit gates without parameters. Aborts if a value is given, but is not + * found in the existing ones. + * + * @param op The gate to be checked. + * @param target The Values of the target qubits. + * @param ctrlsQuantum An array of the values of the ctrl qubits. + * @param posCtrlsClassical An array of the values of the ctrl bits. + * @param negCtrlsClassical An array of the values of the negative ctrl bits. + * @returns An optional containing the globally added value, if applicable. + */ + [[nodiscard("UnionTable::globalPhaseThatIsAdded called but ignored")]] + std::optional + globalPhaseThatIsAdded(Operation* op, Value target, + std::span ctrlsQuantum = {}, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}); + + /** + * @brief This method checks which qubits and classical values are superfluous + * given a controlled gate. + * + * This method checks which qubits and classical values are superfluous given + * a controlled gate. If the gate can never be executed, the SuperfluousResult + * will indicate that the gate is completely superfluous. Apart from that, all + * posCtrl (negCtrl) qubits/values that are always true (false) are + * superfluous. + * + * @param qubitCtrls The values of the positively controlling qubits. + * @param posCtrlsClassical The values of the positively controlling classical + * values. + * @param negCtrlsClassical The values of the negatively controlling classical + * values. + * @returns The superfluous result, i.e. the qubits and classical values that + * are superfluous and whether the whole operation is superfluous. + */ + SuperfluousResult + getSuperfluousControls(std::span qubitCtrls, + std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}); + + /** + * @brief This method checks whether there are satisfiable combinations of + * controls. + * + * @param qubitCtrls The values of the controlling qubit values. + * @param posCtrlsClassical The values of the positively controlling classical + * values. + * @param negCtrlsClassical The values of the negatively controlling classical + * values. + * @returns Whether there are satisfiable combinations or not. + */ + bool areThereSatisfiableCombinations( + std::span qubitCtrls, std::span posCtrlsClassical = {}, + std::span negCtrlsClassical = {}) const; + + /** + * @brief Returns whether the given qubits and classical values that imply the + * given qubit. + * + * This method checks whether in the given list are qubits or classical values + * that imply (are antecedents of) the given qubit. I.e. if there are + * qubits/values a for which holds: a -> q. + * + * @param q The qubit for which is checked whether it is implied. + * @param qubits The qubits for which are checked if they imply q. + * @param classicalPositive The values for which are checked if they imply q. + * @param classicalNegative The values for which their negations are checked + * if they imply q. + * @returns A pair of 1. qubits and 2. classical values that are antecedents + * of q. + */ + bool isQubitImplied(Value q, std::span qubits, + std::span classicalPositive, + std::span classicalNegative) const; +}; +} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 897c05d01c..6b28edfb7e 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -180,6 +180,64 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { // Optimization Passes //===----------------------------------------------------------------------===// +def ConstantPropagation : Pass<"constant-propagation", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect", + "::mlir::arith::ArithDialect"]; + let summary = + "This pass applies constant propagation to a circuit. It " + "assumes that all input qubits are |0>. It propagates the " + "state of the qubits up to a given complexity threshold and " + "removes gates which are superfluous considering the current " + "state. It also replaces quantum control with classical control " + "if possible and moves measurements as far to the front as possible."; + let description = [{ + This pass applies quantum constant propagation. This optimization routine assumes that the input qubits of the + circuits are |0>. It propagates the qubit states and the state of additional classical values through the circuit. + All quantum instructions are removed which are superfluous considering the current state. Additionally, quantum + controls can be replaced by equivalent classical control. To do this most efficiently, the measurements are moved + as far to the front of a circuit as possible. + + The qubit states and classical values are stored in hybrid states. Hybrid states are stored in a union table to + reduce the amount of complex amplitudes and classical values to track. There is a maximum number of non zero + amplitudes that is saved per union table entry. Additionally, there is also a maximum of hybrid states that can be + propagated. If the maximum number of amplitudes or the maximum number of hybrid states is exceeded, the propagated + state reaches top and no optimization routines are further applied. + + The applied optimization routines are: + + **General Control Reduction** + If a controlling qubit or classical value is always true, the control is removed. Classical values are considered + true if they are not zero. + If a controlling qubit or classical value is always false, the complete gate is removed. + + **Unsatisfiable Controls** + If a combination of controls (both quantum and classical) cannot be satisfied, the complete controlled gate is + removed. + + **Quantum Control Reduction** + If a controlling qubit is true if and only if a classical value is true (false), the quantum control is replaced by a + classical one. If the classical value is false if and only if the qubit is true, the classical value becomes a + negative control. + + **Implied Qubits** + If a controlling qubit is implied by other controlling qubits/classical values, the implied qubit is removed from + the controls. + + **Phase Gate Reduction** + If a phase gate would only apply a global phase (e.g. if the quantum state is |11> and a Z-gate is applied), the + phase gate is removed and replaced by a global phase gate instead. + + }]; + let options = [Option<"maximumNonzeroAmplitudes", + "maximum-nonzero-amplitudes", "std::size_t", "4", + "The maximum number of non-zero amplitudes in the " + "tracked quantum states before reaching top.">, + Option<"maximumHybridStates", "maximum-hybrid-states", + "std::size_t", "4", + "The maximum number of hybrid states which have a " + "non-zero probability.">]; +} + def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qco::QCODialect"]; let summary = "This pass attempts to move Hadamard gates as far away from " diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp new file mode 100644 index 0000000000..81b6e7f570 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp @@ -0,0 +1,1176 @@ +/* + * 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/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include "mlir/Dialect/Utils/Utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { +/** + * @brief Result of checking how do modify a controlled gate. + */ +struct ControlsToModify { + llvm::DenseSet quantumCtrlsToRemove; + llvm::DenseSet classicalPosCtrlsToAdd; + llvm::DenseSet classicalNegCtrlsToAdd; +}; +} // namespace + +namespace mlir::qco { + +#define GEN_PASS_DEF_CONSTANTPROPAGATION +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +#define CREATE_OP_CASE_NO_PARAMS(opType) \ + .Case([&](opType gate) { \ + return opType::create(rewriter, gate.getLoc(), qubitsIn[0]); \ + }) + +#define CREATE_OP_CASE_NO_PARAMS_TWO_QUBITS(opType) \ + .Case([&](opType gate) { \ + return opType::create(rewriter, gate.getLoc(), qubitsIn[0], qubitsIn[1]); \ + }) + +#define CREATE_OP_CASE_ONE_PARAM(opType) \ + .Case([&](opType gate) { \ + return opType::create(rewriter, gate.getLoc(), qubitsIn[0], \ + gate.getTheta()); \ + }) + +#define CREATE_OP_CASE_ONE_PARAM_TWO_QUBITS(opType) \ + .Case([&](opType gate) { \ + return opType::create(rewriter, gate.getLoc(), qubitsIn[0], qubitsIn[1], \ + gate.getTheta()); \ + }) + +#define CREATE_OP_CASE_TWO_PARAMS(opType) \ + .Case([&](opType gate) { \ + return opType::create(rewriter, gate.getLoc(), qubitsIn[0], \ + gate.getTheta(), gate.getPhi()); \ + }) + +#define CREATE_OP_CASE_TWO_PARAMS_TWO_QUBITS(opType) \ + .Case([&](opType gate) { \ + return opType::create(rewriter, gate.getLoc(), qubitsIn[0], qubitsIn[1], \ + gate.getTheta(), gate.getPhi()); \ + }) + +#define CREATE_OP_CASE_PLUS_MINUS_OPS(opType) \ + .Case([&](opType gate) { \ + return opType::create(rewriter, gate.getLoc(), qubitsIn[0], qubitsIn[1], \ + gate.getTheta(), gate.getBeta()); \ + }) + +#define CREATE_OP_CASE_THREE_PARAMS(opType) \ + .Case([&](opType gate) { \ + return opType::create(rewriter, gate.getLoc(), qubitsIn[0], \ + gate.getTheta(), gate.getPhi(), gate.getLambda()); \ + }) + +static LogicalResult iterateThroughWorklist(PatternRewriter& rewriter, + UnionTable* ut, + std::span& worklist, + std::span posClassicalCtrls, + std::span negClassicalCtrls); + +/** + * This method checks whether the func::FuncOp is an entry point to the program. + * + * @param op The func::FuncOp to be checked. + * @return Whether the operation is an entry point to the program. + */ +static bool isEntryPoint(const func::FuncOp op) { + const auto passthroughAttr = op->getAttrOfType("passthrough"); + if (!passthroughAttr) { + return false; + } + + return llvm::any_of(passthroughAttr, [](const Attribute attr) { + return mlir::isa(attr) && + mlir::cast(attr) == "entry_point"; + }); +} + +/** + * This method moves all measurements as far to the front as possible, in order + * to execute constant propagation more efficiently. + * + * @param module The module which contains the operations + * @param ctx The MLIR context + */ +static void moveMeasurementsToFront(ModuleOp module, MLIRContext* ctx) { + bool changed = false; + do { + changed = false; + PatternRewriter rewriter(ctx); + module.walk([&](MeasureOp op) { + Operation* previousInstruction = op.getQubitIn().getDefiningOp(); + Operation* previousNode = op->getPrevNode(); + if (previousInstruction == nullptr) { + return; + } + while (previousNode != nullptr && isa(previousNode) && + previousInstruction != previousNode) { + previousNode = previousNode->getPrevNode(); + } + if (previousNode != previousInstruction) { + rewriter.moveOpAfter(op, previousInstruction); + changed = true; + } + }); + } while (changed); +} + +/** + * Removes a UnitaryOpInterface from the mlir context. + * + * @param op The qco::UnitaryOpInterface to be removed. + * @param rewriter The used rewriter. + */ +static void removeOperation(UnitaryOpInterface* op, PatternRewriter& rewriter) { + for (const auto outQubit : op->getOutputQubits()) { + rewriter.replaceAllUsesWith(outQubit, op->getInputForOutput(outQubit)); + } + rewriter.eraseOp(*op); +} + +/** + * Removes a CtrlOp from the mlir context. + * + * @param op The qco::CtrlOp to be removed. + * @param rewriter The used rewriter. + */ +static void removeCtrlOperation(CtrlOp* op, PatternRewriter& rewriter) { + for (const auto outQubit : op->getOutputQubits()) { + rewriter.replaceAllUsesWith(outQubit, op->getInputForOutput(outQubit)); + } + rewriter.eraseOp(*op); +} + +/** + * Moves either the then or else block of an if operation out of the operation. + * Removes the terminator of the block both from the block and the worklist. + * + * @param ifOperation The operation whose then or else block gets inlined. + * @param block The block that gets inlined. + * @param worklist The worklist which contains the operations that are iterated + * through. + * @param rewriter The used rewriter. + */ +static void inlineBranchBlock(IfOp* ifOperation, Block* block, + std::span& worklist, + PatternRewriter& rewriter) { + auto* const operation = ifOperation->getOperation(); + Operation* terminator = block->getTerminator(); + const auto results = terminator->getOperands(); + rewriter.inlineBlockBefore(block, operation, ifOperation->getQubits()); + rewriter.replaceOp(operation, results); + rewriter.eraseOp(terminator); + std::ranges::replace(worklist, terminator, static_cast(nullptr)); +} + +/** + * Handles a constant operation, meaning it is propagated through the union + * table. + * + * @param ut Union table which contains the current quantum state + * @param op The arith::ConstantOp which is propagated. + * @param posClassicalCtrls The positive classical controls considered in the + * operation. + * @param negClassicalCtrls The negative classical controls considered in the + * operation. + * @return Whether the handling was successfully or interrupted. + */ +static WalkResult handleConstant(UnionTable* ut, arith::ConstantOp op, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls) { + if (!posClassicalCtrls.empty() || !negClassicalCtrls.empty()) { + llvm::report_fatal_error("Cannot handle constant operation in conditional " + "branches during constant propagation."); + } + + Value const res = op.getResult(); + auto attr = op.getValue(); + if (const auto intAttr = dyn_cast(attr)) { + ut->propagateIntAlloc(res, intAttr.getInt()); + } + if (const auto doubleAttr = dyn_cast(attr)) { + ut->propagateDoubleAlloc(res, doubleAttr.getValueAsDouble()); + } + if (const auto boolAttr = dyn_cast(attr)) { + if (boolAttr.getValue()) { + ut->propagateIntAlloc(res, 1); + } else { + ut->propagateIntAlloc(res, 0); + } + } + return WalkResult::advance(); +} + +/** + * Checks if only a global phase is added to the quantum machine state. If yes + * and if the global phase is not = 1, it adds a global phase gate. + * + * @param ut Union table which contains the current quantum state + * @param op The qco::UnitaryOpInterface which is propagated. + * @param ctrlsQuantum The quantum control values considered in the operation. + * @param posClassicalCtrls The positive classical controls considered in the + * operation. + * @param negClassicalCtrls The negative classical controls considered in the + * operation. + * @param rewriter The used rewriter. + * @param targetValues The target values (non-empty if the method was called via + * a quantum control (qco::CtrlOp). + * @return Whether there is only a global phase added. + */ +static bool addsOnlyGlobalPhase(UnionTable* ut, UnitaryOpInterface* op, + const std::span ctrlsQuantum, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls, + PatternRewriter& rewriter, + const std::span targetValues) { + bool addsGlobalPhase = false; + if (isa(op) || isa(op) || isa(op) || isa(op) || + isa(op) || isa(op)) { + const auto inputQubit = + targetValues.empty() ? op->getInputQubit(0) : targetValues[0]; + const auto addedGlobalPhase = ut->globalPhaseThatIsAdded( + *op, inputQubit, ctrlsQuantum, posClassicalCtrls, negClassicalCtrls); + if (addedGlobalPhase.has_value()) { + addsGlobalPhase = true; + const auto phase = addedGlobalPhase.value(); + if (std::norm(phase) > 1e-4) { + GPhaseOp::create(rewriter, op->getLoc(), phase); + } + } + } + return addsGlobalPhase; +} + +/** + * Creates a new gate at the location of the given gate and of the type of the + * given gate. + * + * @param op The operation whose type and location is used. + * @param rewriter The used rewriter. + * @param qubitsIn A span of target inputs. + * @return The newly created gate. + */ +static Operation* +createOperationFromUnitaryOperation(Operation* op, PatternRewriter& rewriter, + const std::span qubitsIn) { + // clang-format off + Operation *newOp = mlir::TypeSwitch(op) + .Case([&](U2Op gate) { + return U2Op::create(rewriter, gate.getLoc(), qubitsIn[0], + gate.getPhi(), gate.getLambda()); + }) + CREATE_OP_CASE_NO_PARAMS(IdOp) + CREATE_OP_CASE_NO_PARAMS(HOp) + CREATE_OP_CASE_NO_PARAMS(XOp) + CREATE_OP_CASE_NO_PARAMS(YOp) + CREATE_OP_CASE_NO_PARAMS(ZOp) + CREATE_OP_CASE_NO_PARAMS(SOp) + CREATE_OP_CASE_NO_PARAMS(SdgOp) + CREATE_OP_CASE_NO_PARAMS(TOp) + CREATE_OP_CASE_NO_PARAMS(TdgOp) + CREATE_OP_CASE_NO_PARAMS(SXOp) + CREATE_OP_CASE_NO_PARAMS(SXdgOp) + CREATE_OP_CASE_ONE_PARAM(RXOp) + CREATE_OP_CASE_ONE_PARAM(RYOp) + CREATE_OP_CASE_ONE_PARAM(RZOp) + CREATE_OP_CASE_ONE_PARAM(POp) + CREATE_OP_CASE_TWO_PARAMS(ROp) + CREATE_OP_CASE_THREE_PARAMS(UOp) + CREATE_OP_CASE_NO_PARAMS_TWO_QUBITS(SWAPOp) + CREATE_OP_CASE_NO_PARAMS_TWO_QUBITS(iSWAPOp) + CREATE_OP_CASE_NO_PARAMS_TWO_QUBITS(DCXOp) + CREATE_OP_CASE_NO_PARAMS_TWO_QUBITS(ECROp) + CREATE_OP_CASE_ONE_PARAM_TWO_QUBITS(RXXOp) + CREATE_OP_CASE_ONE_PARAM_TWO_QUBITS(RYYOp) + CREATE_OP_CASE_ONE_PARAM_TWO_QUBITS(RZXOp) + CREATE_OP_CASE_ONE_PARAM_TWO_QUBITS(RZZOp) + CREATE_OP_CASE_PLUS_MINUS_OPS(XXPlusYYOp) + CREATE_OP_CASE_PLUS_MINUS_OPS(XXMinusYYOp) + .Default([&](Operation *unsupported) -> Operation * { + llvm::report_fatal_error( + ("Unsupported operation: " + unsupported->getName() + .getStringRef() + .str()) + .c_str()); + }); + // clang-format on + + return newOp; +} + +/** + * Removes all controls from a gate and returns an uncontrolled operation. + * + * @param op The qco::CtrlOp whose controls are removed. + * @param rewriter The used rewriter + * @param worklist The worklist which contains the operations that are iterated + * through. + * @return The operation without the controls. + */ +static UnitaryOpInterface +removeAllCtrlsOfGate(CtrlOp* op, PatternRewriter& rewriter, + std::span& worklist) { + for (const auto& qubitCtrl : op->getInputQubits()) { + rewriter.replaceAllUsesWith(op->getOutputForInput(qubitCtrl), qubitCtrl); + } + + const auto innerUnitary = + utils::getSoleBodyUnitary(*op->getBody()); + + const auto targetInput = op->getInputTargets(); + std::vector qubitsIn = {targetInput.begin(), targetInput.end()}; + auto* const newOp = + createOperationFromUnitaryOperation(innerUnitary, rewriter, qubitsIn); + auto newUnitary = static_cast(newOp); + for (const auto inTarget : newUnitary.getInputQubits()) { + rewriter.replaceAllUsesExcept( + inTarget, newUnitary.getOutputForInput(inTarget), newUnitary); + } + rewriter.eraseOp(*op); + std::ranges::replace(worklist, *op, newOp); + + return newUnitary; +} + +/** + * Removes the given quantum controls from a CtrlOp, but not removing all + * controls and only leaving the (formerly controlled) gate in the body. + * + * @param op The qco::CtrlOp whose controls are removed. + * @param ctrlsToRemove The controls which should be removed from the CtrlOp. + * @param rewriter The used rewriter + * @param worklist The worklist which contains the operations that are iterated + * through. + * @return The operation without given controls. + */ +static CtrlOp removeCtrlsOfGate(CtrlOp* op, + const llvm::DenseSet& ctrlsToRemove, + PatternRewriter& rewriter, + std::span& worklist) { + for (const auto& qubitCtrl : ctrlsToRemove) { + rewriter.replaceAllUsesWith(op->getOutputForInput(qubitCtrl), qubitCtrl); + } + if (ctrlsToRemove.size() == op->getNumControls()) { + llvm::report_fatal_error("Method not suitable to remove all controls of a " + "CtrlOp. Should not be called for this."); + } + std::vector newControlIn; + for (const auto& ctrls : op->getInputControls()) { + if (!ctrlsToRemove.contains(ctrls)) { + newControlIn.push_back(ctrls); + } + } + const auto innerUnitary = + utils::getSoleBodyUnitary(*op->getBody()); + CtrlOp newCtrl = CtrlOp::create( + rewriter, op->getLoc(), newControlIn, op->getTargetsIn(), + [&](const ValueRange target) { + std::vector qubitsIn = {target.begin(), target.end()}; + auto* const newOp = createOperationFromUnitaryOperation( + innerUnitary, rewriter, qubitsIn); + return SmallVector{newOp->getResults()}; + }); + + for (const auto inTarget : newCtrl.getInputQubits()) { + rewriter.replaceAllUsesWith(op->getOutputForInput(inTarget), + newCtrl.getOutputForInput(inTarget)); + } + for (const auto ctrlQubit : op->getOutputControls()) { + rewriter.replaceAllUsesWith(ctrlQubit, op->getInputForOutput(ctrlQubit)); + } + rewriter.eraseOp(*op); + std::ranges::replace(worklist, *op, newCtrl); + + return newCtrl; +} + +/** + * Handles classical branching. Iterates through the body of the branching in a + * new loop and removes the body from the current iteration. + * + * @param ut Union table which contains the current quantum state + * @param op The qco::UnitaryOpInterface which is propagated. + * @param posClassicalCtrls The positive classical controls considered in the + * operation. + * @param negClassicalCtrls The negative classical controls considered in the + * operation. + * @param rewriter The used rewriter + * @param worklist The worklist which contains the operations that are iterated + * through. + * @return Whether the handling was successfully or interrupted. + */ +static WalkResult handleIfOp(UnionTable* ut, IfOp* op, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls, + PatternRewriter& rewriter, + std::span& worklist) { + const Value condition = op->getCondition(); + + // Remove branching if value is always or never true + if (ut->isClassicalValueAlwaysTrue(condition)) { + op->elseBlock()->walk([&](Operation* innerOp) { + std::ranges::replace(worklist, innerOp, static_cast(nullptr)); + }); + + Block* block = &op->getThenRegion().front(); + inlineBranchBlock(op, block, worklist, rewriter); + + return WalkResult::advance(); + } + if (ut->isClassicalValueAlwaysFalse(condition)) { + op->thenBlock()->walk([&](Operation* innerOp) { + std::ranges::replace(worklist, innerOp, static_cast(nullptr)); + }); + + Block* block = &op->getElseRegion().front(); + inlineBranchBlock(op, block, worklist, rewriter); + + return WalkResult::advance(); + } + + Block* thenBlock = op->thenBlock(); + Block* elseBlock = op->elseBlock(); + bool thenEmpty = thenBlock->getOperations().size() <= 1; + bool elseEmpty = elseBlock->getOperations().size() <= 1; + + const auto targetQubits = op->getQubits(); + std::vector targets = {targetQubits.begin(), targetQubits.end()}; + std::vector thenArgs; + std::vector elseArgs; + + std::vector newThenWorklist; + std::vector newElseWorklist; + + // Create a new worklist to iterate over the inner instructions + op->thenBlock()->walk([&](Operation* innerOp) { + newThenWorklist.push_back(innerOp); + std::ranges::replace(worklist, innerOp, static_cast(nullptr)); + }); + op->elseBlock()->walk([&](Operation* innerOp) { + newElseWorklist.push_back(innerOp); + std::ranges::replace(worklist, innerOp, static_cast(nullptr)); + }); + + // propagate through then and else block + if (!thenEmpty) { + for (const Value arg : thenBlock->getArguments()) { + thenArgs.push_back(arg); + } + ut->replaceValuesGlobally(targets, thenArgs); + + std::span wl(newThenWorklist.data(), newThenWorklist.size()); + std::vector newPosClassicalCtrls = {posClassicalCtrls.begin(), + posClassicalCtrls.end()}; + newPosClassicalCtrls.push_back(condition); + const auto resThen = iterateThroughWorklist( + rewriter, ut, wl, newPosClassicalCtrls, negClassicalCtrls); + + op->thenBlock()->walk([&](Operation* innerOp) { + const auto input = innerOp->getOperands(); + const auto output = innerOp->getResults(); + for (unsigned int i = 0; i < std::min(input.size(), output.size()); ++i) { + if (mlir::isa(input[i].getType())) { + std::ranges::replace(thenArgs, input[i], output[i]); + } + } + }); + + if (resThen.failed()) { + return WalkResult::interrupt(); + } + } else { + const auto linearThenValues = op->thenYield().getTargets().drop_front( + op->getClassicalResults().size()); + thenArgs = {linearThenValues.begin(), linearThenValues.end()}; + } + + if (!elseEmpty) { + for (const Value arg : elseBlock->getArguments()) { + elseArgs.push_back(arg); + } + ut->replaceValuesGlobally(thenEmpty ? targets : thenArgs, elseArgs); + + std::span wl(newElseWorklist.data(), newElseWorklist.size()); + std::vector newNegClassicalCtrls = {negClassicalCtrls.begin(), + negClassicalCtrls.end()}; + newNegClassicalCtrls.push_back(condition); + const auto resElse = iterateThroughWorklist( + rewriter, ut, wl, posClassicalCtrls, newNegClassicalCtrls); + + if (resElse.failed()) { + return WalkResult::interrupt(); + } + } + const auto linearElseValues = + op->elseYield().getTargets().drop_front(op->getClassicalResults().size()); + elseArgs = {linearElseValues.begin(), linearElseValues.end()}; + + const auto resultQubits = op->getResults(); + std::vector results = {resultQubits.begin(), resultQubits.end()}; + + thenBlock = op->thenBlock(); + elseBlock = op->elseBlock(); + thenEmpty = thenBlock->getOperations().size() <= 1; + elseEmpty = elseBlock->getOperations().size() <= 1; + + // Remove if operation completely if both branches are empty after propagation + if (thenEmpty && elseEmpty) { + // Check that there is no implicit swap in one branch by re-ordered yield + // operands and get order of returned qubits + std::vector order; + bool implicitSwap = false; + for (unsigned int i = 0; i < thenBlock->getArguments().size(); ++i) { + auto it = std::ranges::find(thenArgs, thenBlock->getArguments()[i]); + if (it != thenArgs.end()) { + const unsigned int pos = std::distance(thenArgs.begin(), it); + order.push_back(pos); + } + } + + for (unsigned int i = 0; i < elseBlock->getArguments().size(); ++i) { + auto it = std::ranges::find(elseArgs, elseBlock->getArguments()[i]); + if (it != elseArgs.end()) { + const unsigned int pos = std::distance(elseArgs.begin(), it); + if (!thenArgs.empty()) { + implicitSwap |= order.at(i) != pos; + } else { + order.push_back(pos); + } + } + } + + if (implicitSwap) { + llvm::report_fatal_error("Constant propagation does not allow implicit " + "swapping of qubits in branching."); + } + // remove if Op and replace the values in the module and union table + std::ranges::replace(worklist, *op, static_cast(nullptr)); + for (unsigned int inputQubitIndex = 0; + inputQubitIndex < op->getQubits().size(); ++inputQubitIndex) { + rewriter.replaceAllUsesWith( + op->getLinearResults()[order.at(inputQubitIndex)], + op->getQubits()[inputQubitIndex]); + } + + std::vector inputQubitVec = {op->getQubits().begin(), + op->getQubits().end()}; + ut->replaceValuesGlobally(elseArgs.empty() ? thenArgs : elseArgs, + inputQubitVec); + rewriter.eraseOp(*op); + } else { + ut->replaceValuesGlobally(elseEmpty ? thenArgs : elseArgs, results); + } + + return WalkResult::advance(); +} + +/** + * Puts the given operation into a classical branch and propagates the branch. + * Only supports one new condition, i.e. only considers the first positive or + * negative new controls. + * + * @param ut Union table which contains the current quantum state. + * @param op The operation to be put in a classical branch. + * @param posClassicalCtrls The positive classical controls considered in the + * operation. + * @param negClassicalCtrls The negative classical controls considered in the + * operation. + * @param ctrlsToMod The controls which need to be used in the branch. + * @param rewriter The used rewriter. + * @param worklist The worklist which contains the operations that are iterated + * through. + * @return Whether the propagation of the new operations in a branch succeeded. + */ +static WalkResult +putOperationIntoBranch(UnionTable* ut, UnitaryOpInterface op, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls, + ControlsToModify ctrlsToMod, PatternRewriter& rewriter, + std::span& worklist) { + Value condition = ctrlsToMod.classicalNegCtrlsToAdd.empty() + ? *ctrlsToMod.classicalPosCtrlsToAdd.begin() + : *ctrlsToMod.classicalNegCtrlsToAdd.begin(); + const auto numberOfNewConditions = ctrlsToMod.classicalPosCtrlsToAdd.size() + + ctrlsToMod.classicalNegCtrlsToAdd.size(); + if (numberOfNewConditions > 1) { + // Create classical operations to consider all new classical controls + std::vector conditions; + conditions.reserve(ctrlsToMod.classicalPosCtrlsToAdd.size()); + std::ranges::copy(ctrlsToMod.classicalPosCtrlsToAdd, + std::back_inserter(conditions)); + bool firstNegVal = true; + arith::ConstantOp zero; + for (const auto negVal : ctrlsToMod.classicalNegCtrlsToAdd) { + if (firstNegVal) { + firstNegVal = false; + auto i1Ty = rewriter.getI1Type(); + zero = arith::ConstantOp::create(rewriter, op->getLoc(), + rewriter.getIntegerAttr(i1Ty, 0)); + ut->propagateIntAlloc(zero, 0); + } + const auto negatedValOp = arith::CmpIOp::create( + rewriter, op->getLoc(), arith::CmpIPredicate::eq, negVal, zero); + const auto negatedVal = negatedValOp->getResult(0); + std::vector negationOperands = { + negatedValOp->getOperands().begin(), + negatedValOp->getOperands().end()}; + std::vector negationResults = {negatedValOp->getResults().begin(), + negatedValOp->getResults().end()}; + ut->propagateClassicalOperation(negatedValOp, negationOperands, + negationResults, posClassicalCtrls, + negClassicalCtrls); + conditions.push_back(negatedVal); + } + auto andOp = arith::AndIOp::create(rewriter, op->getLoc(), conditions[0], + conditions[1]); + condition = andOp->getResult(0); + std::vector operands = {andOp->getOperands().begin(), + andOp->getOperands().end()}; + std::vector results = {andOp->getResults().begin(), + andOp->getResults().end()}; + ut->propagateClassicalOperation(andOp, operands, results, posClassicalCtrls, + negClassicalCtrls); + + for (unsigned int classicalConditionIndex = 2; + classicalConditionIndex < conditions.size(); + ++classicalConditionIndex) { + andOp = arith::AndIOp::create(rewriter, op->getLoc(), condition, + conditions[classicalConditionIndex]); + condition = andOp->getResult(0); + operands = {andOp->getOperands().begin(), andOp->getOperands().end()}; + results = {andOp->getResults().begin(), andOp->getResults().end()}; + ut->propagateClassicalOperation(andOp, operands, results, + posClassicalCtrls, negClassicalCtrls); + } + } + + const bool createThenBranch = + numberOfNewConditions > 1 || !ctrlsToMod.classicalPosCtrlsToAdd.empty(); + ValueRange insertedQubits = op.getInputQubits(); + const SmallVector locs(insertedQubits.size(), op->getLoc()); + auto newIfOp = + IfOp::create(rewriter, op->getLoc(), condition, insertedQubits); + + IRMapping map; + + if (createThenBranch) { + auto* thenBlock = rewriter.createBlock(&newIfOp.getThenRegion(), {}, + newIfOp->getResultTypes(), locs); + rewriter.setInsertionPointToStart(thenBlock); + for (auto [originalInput, ifArgs] : + llvm::zip(insertedQubits, thenBlock->getArguments())) { + map.map(originalInput, ifArgs); + } + auto* thenClone = rewriter.clone(*op.getOperation(), map); + YieldOp::create(rewriter, op->getLoc(), thenClone->getResults()); + + auto* elseBlock = rewriter.createBlock(&newIfOp.getElseRegion(), {}, + newIfOp->getResultTypes(), locs); + YieldOp::create(rewriter, op->getLoc(), elseBlock->getArguments()); + } else { + auto* elseBlock = rewriter.createBlock(&newIfOp.getElseRegion(), {}, + newIfOp->getResultTypes(), locs); + rewriter.setInsertionPointToStart(elseBlock); + for (auto [originalInput, ifArgs] : + llvm::zip(insertedQubits, elseBlock->getArguments())) { + map.map(originalInput, ifArgs); + } + auto* elseClone = rewriter.clone(*op.getOperation(), map); + YieldOp::create(rewriter, op->getLoc(), elseClone->getResults()); + + auto* thenBlock = rewriter.createBlock(&newIfOp.getThenRegion(), {}, + newIfOp->getResultTypes(), locs); + YieldOp::create(rewriter, op->getLoc(), thenBlock->getArguments()); + } + + rewriter.replaceAllUsesWith(op.getOutputQubits(), newIfOp.getResults()); + rewriter.replaceOp(op.getOperation(), newIfOp.getResults()); + + return handleIfOp(ut, &newIfOp, posClassicalCtrls, negClassicalCtrls, + rewriter, worklist); +} + +/** + * Handles a unitary gate, meaning it is propagated through the union table. + * + * @param ut Union table which contains the current quantum state + * @param op The qco::UnitaryOpInterface which is propagated. + * @param ctrlsQuantum The quantum control values considered in the operation. + * @param newCtrlsQuantum The values the quantum control values become after + * the operation. + * @param posClassicalCtrls The positive classical controls considered in the + * operation. + * @param negClassicalCtrls The negative classical controls considered in the + * operation. + * @param targetValues The target values (non-empty if the method was called via + * a quantum control (qco::CtrlOp). + * @param resultValues The values the target values become after the operation. + * @return Whether the handling was successfully or interrupted. + */ +static WalkResult handleUnitary(UnionTable* ut, UnitaryOpInterface* op, + const std::span ctrlsQuantum, + const std::span newCtrlsQuantum, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls, + const std::span targetValues = {}, + const std::span resultValues = {}) { + + const auto params = op->getParameters(); + std::vector paramValues = {params.begin(), params.end()}; + if (targetValues.empty() && resultValues.empty()) { + const auto targets = op->getInputTargets(); + const auto results = op->getOutputTargets(); + std::vector targetVecs = {targets.begin(), targets.end()}; + std::vector resultVecs = {results.begin(), results.end()}; + ut->propagateGate(*op, targetVecs, resultVecs, ctrlsQuantum, + newCtrlsQuantum, posClassicalCtrls, negClassicalCtrls, + paramValues); + } else if (targetValues.size() == resultValues.size()) { + ut->propagateGate(*op, targetValues, resultValues, ctrlsQuantum, + newCtrlsQuantum, posClassicalCtrls, negClassicalCtrls, + paramValues); + } else { + llvm::report_fatal_error( + "Given targetValues and resultValues need to be of same size."); + } + + return WalkResult::advance(); +} + +/** + * Handles an uncontrolled unitary gate. First, it is checked whether the gate + * only adds a global phase in case it is a diagonal gate. Then the gate is + * either propagated on the union table or removed/replaced by a gloal phase + * gate. + * + * @param ut Union table which contains the current quantum state + * @param op The qco::UnitaryOpInterface which is propagated. + * @param posClassicalCtrls The positive classical controls considered in the + * operation. + * @param negClassicalCtrls The negative classical controls considered in the + * operation. + * @param rewriter The used rewriter + * @param worklist The worklist which contains the operations that are iterated + * through. + * @return Whether the handling was successfully or interrupted. + */ +static WalkResult +handleUncontrolledUnitary(UnionTable* ut, UnitaryOpInterface* op, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls, + PatternRewriter& rewriter, + std::span& worklist) { + if (isa(op)) { + return WalkResult::advance(); + } + const auto targets = op->getInputTargets(); + std::vector targetVecs = {targets.begin(), targets.end()}; + + // Check if a diagonal gate only adds a global phase + const bool addsGlobalPhase = addsOnlyGlobalPhase( + ut, op, {}, posClassicalCtrls, negClassicalCtrls, rewriter, targetVecs); + if (addsGlobalPhase) { + std::ranges::replace(worklist, *op, static_cast(nullptr)); + removeOperation(op, rewriter); + return WalkResult::advance(); + } + + return handleUnitary(ut, op, {}, {}, posClassicalCtrls, negClassicalCtrls); +} + +/** + * Handles a CtrlOp. First, it is checked whether the CtrlOp is executable + * considering the current quantum machine state. Then the CtrlOp is either + * propagated on the union table or removed/replaced by an unconditional gate. + * If it is replaced by an unconditional gate, the gate is propagated. + * + * @param ut Union table which contains the current quantum state + * @param op The qco::CtrlOp which is propagated. + * @param posClassicalCtrls The positive classical controls considered in the + * operation. + * @param negClassicalCtrls The negative classical controls considered in the + * operation. + * @param rewriter The used rewriter + * @param worklist The worklist which contains the operations that are iterated + * through. + * @return Whether the handling was successfully or interrupted. + */ +static WalkResult handleCtrlOp(UnionTable* ut, CtrlOp* op, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls, + PatternRewriter& rewriter, + std::span& worklist) { + // Avoid to address body twice + op->walk([&](Operation* bodyOp) { + std::ranges::replace(worklist, bodyOp, static_cast(nullptr)); + }); + + const auto inputCtrls = op->getInputControls(); + std::vector inCtrlValues = {inputCtrls.begin(), inputCtrls.end()}; + + // Check if gate is executable + const auto satisfiable = ut->areThereSatisfiableCombinations( + inCtrlValues, posClassicalCtrls, negClassicalCtrls); + if (!satisfiable) { + std::ranges::replace(worklist, *op, static_cast(nullptr)); + removeCtrlOperation(op, rewriter); + return WalkResult::advance(); + } + const auto superfluousCtrls = ut->getSuperfluousControls( + inCtrlValues, posClassicalCtrls, negClassicalCtrls); + if (superfluousCtrls.completelySuperfluous) { + std::ranges::replace(worklist, *op, static_cast(nullptr)); + removeCtrlOperation(op, rewriter); + return WalkResult::advance(); + } + + // Collect quantum values to remove and classical values to add + ControlsToModify ctrlsToMod; + ctrlsToMod.quantumCtrlsToRemove = superfluousCtrls.superfluousQubits; + for (const auto superfluousQ : ctrlsToMod.quantumCtrlsToRemove) { + std::erase(inCtrlValues, superfluousQ); + } + + const auto ctrlCandidates = inCtrlValues; + for (const auto qCtrl : ctrlCandidates) { + auto qCtrlValuesWithoutCurrent = inCtrlValues; + std::erase(qCtrlValuesWithoutCurrent, qCtrl); + if (ut->isQubitImplied(qCtrl, qCtrlValuesWithoutCurrent, posClassicalCtrls, + negClassicalCtrls)) { + std::erase(inCtrlValues, qCtrl); + ctrlsToMod.quantumCtrlsToRemove.insert(qCtrl); + } else if (auto v = ut->getValueThatIsEquivalentToQubit(qCtrl); + !v.empty()) { + for (const auto& [value, b] : v) { + if (b) { + ctrlsToMod.classicalPosCtrlsToAdd.insert(value); + } else { + ctrlsToMod.classicalNegCtrlsToAdd.insert(value); + } + ctrlsToMod.quantumCtrlsToRemove.insert(qCtrl); + break; + } + } + } + + if (!ctrlsToMod.quantumCtrlsToRemove.empty()) { + if (ctrlsToMod.quantumCtrlsToRemove.size() == op->getNumControls()) { + auto newOp = removeAllCtrlsOfGate(op, rewriter, worklist); + if (!ctrlsToMod.classicalPosCtrlsToAdd.empty() || + !ctrlsToMod.classicalNegCtrlsToAdd.empty()) { + return putOperationIntoBranch(ut, newOp, posClassicalCtrls, + negClassicalCtrls, ctrlsToMod, rewriter, + worklist); + } + return handleUncontrolledUnitary(ut, &newOp, posClassicalCtrls, + negClassicalCtrls, rewriter, worklist); + } + *op = removeCtrlsOfGate(op, ctrlsToMod.quantumCtrlsToRemove, rewriter, + worklist); + if (!ctrlsToMod.classicalPosCtrlsToAdd.empty() || + !ctrlsToMod.classicalNegCtrlsToAdd.empty()) { + return putOperationIntoBranch(ut, *op, posClassicalCtrls, + negClassicalCtrls, ctrlsToMod, rewriter, + worklist); + } + } + + auto body = utils::getSoleBodyUnitary(*op->getBody()); + // Make sure that the right qubits in the right order are passed to the + // propagation of the body unitary + std::vector targetQubits; + const auto numTargets = op->getNumTargets(); + targetQubits.resize(numTargets); + const auto arguments = op->getRegion().getArguments(); + for (unsigned int argIndex = 0; argIndex < arguments.size(); ++argIndex) { + for (unsigned int i = 0; i < numTargets; ++i) { + if (arguments[argIndex] == body.getInputTarget(i)) { + targetQubits[i] = op->getInputTarget(argIndex); + break; + } + } + } + + std::vector resultQubits; + resultQubits.resize(numTargets); + const auto yieldOP = cast(*op->getBody()->rbegin()); + for (unsigned int uOpOutIndex = 0; uOpOutIndex < numTargets; ++uOpOutIndex) { + for (unsigned int i = 0; i < numTargets; ++i) { + if (yieldOP->getOperand(i) == body.getOutputTarget(uOpOutIndex)) { + resultQubits[i] = op->getOutputTarget(uOpOutIndex); + break; + } + } + } + + const auto outputCtrls = op->getOutputControls(); + std::vector outCtrlValues = {outputCtrls.begin(), outputCtrls.end()}; + + // Check if a diagonal gate only adds a global phase + const bool addsGlobalPhase = + addsOnlyGlobalPhase(ut, &body, inCtrlValues, posClassicalCtrls, + negClassicalCtrls, rewriter, targetQubits); + if (addsGlobalPhase) { + std::ranges::replace(worklist, *op, static_cast(nullptr)); + removeCtrlOperation(op, rewriter); + return WalkResult::advance(); + } + + return handleUnitary(ut, &body, inCtrlValues, outCtrlValues, + posClassicalCtrls, negClassicalCtrls, targetQubits, + resultQubits); +} + +/** + * Iterates through the worklist of operators and propagates the quantum machine + * state through the union table. The iteration can be called with specific + * classical controls which are considered in every propagation. + * + * @param rewriter The used rewriter + * @param ut The union table which contains the current quantum machine state. + * @param worklist The worklist which contains the operations that are iterated + * through. + * @param posClassicalCtrls The positive classical controls considered in every + * operation. + * @param negClassicalCtrls The negative classical controls considered in every + * operation. + * @return Whether the iteration was successfully or interrupted. + */ +static LogicalResult +iterateThroughWorklist(PatternRewriter& rewriter, UnionTable* ut, + std::span& worklist, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls) { + /// Iterate work-list. + bool addedAtLeastOneQubit = false; + for (Operation* curr : worklist) { + if (addedAtLeastOneQubit && ut->areStatesAllTop()) { + return success(); + } + if (curr == nullptr) { + continue; // Skip erased ops. + } + + rewriter.setInsertionPoint(curr); + + const auto res = + TypeSwitch(curr) + /// qco Dialect + .Case([&](CtrlOp op) { + return handleCtrlOp(ut, &op, posClassicalCtrls, negClassicalCtrls, + rewriter, worklist); + }) + .Case([&](UnitaryOpInterface op) { + return handleUncontrolledUnitary(ut, &op, posClassicalCtrls, + negClassicalCtrls, rewriter, + worklist); + }) + .Case([&](ResetOp op) { + ut->propagateReset(op.getOperand(), op.getResult(), + posClassicalCtrls, negClassicalCtrls); + return WalkResult::advance(); + }) + .Case([&](const MeasureOp op) { + ut->propagateMeasurement(op->getOperand(0), op->getResult(0), + op->getResult(1), posClassicalCtrls, + negClassicalCtrls); + return WalkResult::advance(); + }) + .Case([&](AllocOp op) { + addedAtLeastOneQubit = true; + ut->propagateQubitAlloc(op.getResult()); + return WalkResult::advance(); + }) + .Case([&]([[maybe_unused]] SinkOp op) { + return WalkResult::advance(); + }) + .Case([&](IfOp op) { + return handleIfOp(ut, &op, posClassicalCtrls, negClassicalCtrls, + rewriter, worklist); + }) + .Case([&]([[maybe_unused]] YieldOp op) { + return WalkResult::advance(); + }) + // qtensor dialect + .Case([&]([[maybe_unused]] qtensor::AllocOp op) { + return WalkResult::advance(); + }) + .Case( + [&]([[maybe_unused]] qtensor::DeallocOp op) { + return WalkResult::advance(); + }) + .Case([&](const qtensor::ExtractOp op) { + addedAtLeastOneQubit = true; + ut->propagateQubitAlloc(op->getResult(1)); + return WalkResult::advance(); + }) + .Case( + [&]([[maybe_unused]] qtensor::InsertOp op) { + return WalkResult::advance(); + }) + // arith dialect + .Case([&](const arith::ConstantOp op) { + return handleConstant(ut, op, posClassicalCtrls, + negClassicalCtrls); + }) + // func Dialect + .Case([&](const func::FuncOp op) { + if (!isEntryPoint(op)) { + llvm::report_fatal_error( + "Constant propagation does not support nested functions."); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }) + .Case([&]([[maybe_unused]] func::ReturnOp op) { + return WalkResult::advance(); + }) + .Default([ut, posClassicalCtrls, negClassicalCtrls](Operation* op) { + if (llvm::isa(op->getDialect())) { + std::vector operands = {op->getOperands().begin(), + op->getOperands().end()}; + std::vector results = {op->getResults().begin(), + op->getResults().end()}; + ut->propagateClassicalOperation(op, operands, results, + posClassicalCtrls, + negClassicalCtrls); + return WalkResult::advance(); + } + + llvm::report_fatal_error("Unsupported operation"); + return WalkResult::interrupt(); + }); + + if (res.wasInterrupted()) { + return failure(); + } + } + return success(); +} + +/** + * @brief Do constant propagation. + * + * @details + * Collects all functions marked with the 'entry_point' attribute, builds + * a preorder worklist of their operations, and processes that list. + * + * @note + * We consciously avoid MLIR pattern drivers: Idiomatic MLIR + * transformation patterns are independent and order-agnostic. Since we + * require state-sharing between patterns for the transformation we + * violate this assumption. Essentially this is also the reason why we + * can't utilize MLIR's `applyPatternsGreedily` function. Moreover, we + * require pre-order traversal which current drivers of MLIR don't + * support. However, even if such a driver would exist, it would probably + * not return logical results which we require for error-handling + * (similarly to `walkAndApplyPatterns`). Consequently, a custom driver + * would be required in any case, which adds unnecessary code to maintain. + * + * @param module The module which contains the operations + * @param ctx The MLIR context + * @param maxNonzeroAmplitudes The maximum number of non-zero amplitudes in the + * tracked quantum states before reaching top. + * @param maxHybridStates The maximum number of hybrid states which have a + * non-zero probability. + * @return Success if constant propagation has been applied successfully + */ +static LogicalResult applyCP(ModuleOp module, MLIRContext* ctx, + const size_t maxNonzeroAmplitudes, + const size_t maxHybridStates) { + moveMeasurementsToFront(module, ctx); + + PatternRewriter rewriter(ctx); + + /// Prepare work-list. + std::vector worklist; + + bool entryPointFound = false; + for (const auto func : module.getOps()) { + + if (!isEntryPoint(func)) { + continue; // Ignore non entry_point functions for now. + } + if (entryPointFound) { + llvm::report_fatal_error("Constant propagation does not support programs " + "with more than one entry point."); + } + entryPointFound = true; + func->walk( + [&](Operation* op) { worklist.push_back(op); }); + } + + auto ut = UnionTable(maxNonzeroAmplitudes, maxHybridStates); + + std::span wl(worklist.data(), worklist.size()); + + return iterateThroughWorklist(rewriter, &ut, wl, {}, {}); +} + +} // namespace mlir::qco + +namespace { +/** + * This pass applies constant propagation to a circuit. It assumes that all + * states start in |0> and removes quantum instructions that are superfluous + * when the current state is considered. It also replaces quantum resources by + * classical resources. + */ +struct ConstantPropagation final + : mlir::qco::impl::ConstantPropagationBase { + using ConstantPropagationBase::ConstantPropagationBase; + +protected: + void runOnOperation() override { + if (mlir::failed(mlir::qco::applyCP(getOperation(), &getContext(), + maximumNonzeroAmplitudes, + maximumHybridStates))) { + signalPassFailure(); + } + } +}; +} // namespace diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp new file mode 100644 index 0000000000..738efc03b0 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp @@ -0,0 +1,326 @@ +/* + * 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/Transforms/Optimizations/ConstantPropagation/HybridState.hpp" + +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h" +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +HybridState::HybridState(std::span globalQubitNumber, + std::size_t maxNonzeroAmplitudes, + const double probability) + : probability(probability) { + qState = make_shared(globalQubitNumber, maxNonzeroAmplitudes); +} + +HybridState::~HybridState() { + qState.reset(); + integerValues.clear(); + doubleValues.clear(); +} + +void HybridState::print(std::ostream& os) const { os << this->toString(); } +std::string HybridState::toString() const { + if (top) { + return "TOP"; + } + std::ostringstream oss; + + oss << "{" << this->qState->toString() << "}: "; + unsigned int i = 0; + bool first = true; + for (const auto& key : integerValues.keys()) { + if (!first) { + oss << ", "; + } + first = false; + oss << "integerValue" << i << " = " << integerValues.at(key); + ++i; + } + unsigned int j = 0; + for (const auto& key : doubleValues.keys()) { + if (!first) { + oss << ", "; + } + first = false; + + oss << "doubleValue" << j << " = " << std::fixed << std::setprecision(2) + << doubleValues.at(key); + + ++j; + } + if (i > 0 || j > 0) { + oss << "; "; + } + oss << "p = " << std::fixed << std::setprecision(2) << this->probability + << ";"; + return oss.str(); +} + +bool HybridState::operator==(const HybridState& that) const { + if (top || that.top) { + return top == that.top; + } + if (std::fabs(probability - that.probability) > 1e-4 || + *qState.get() != *that.qState.get()) { + return false; + } + + if (integerValues.size() != that.integerValues.size() || + doubleValues.size() != that.doubleValues.size()) { + return false; + } + + for (const auto& [i, v] : integerValues) { + if (!that.integerValues.contains(i) || that.integerValues.at(i) != v) { + return false; + } + } + + return std::ranges::all_of(doubleValues, [&](const auto& p) { + auto it = that.doubleValues.find(p.first); + return it != that.doubleValues.end() && + std::fabs(it->second - p.second) <= 1e-4; + }); +} + +void HybridState::addIntegerValue(const Value value, const int64_t number) { + integerValues[value] = number; +} + +void HybridState::addDoubleValue(const Value value, const double number) { + doubleValues[value] = number; +} + +void HybridState::changeGlobalIndex(const unsigned int target, + const unsigned int newIndex) const { + qState->changeGlobalIndex(target, newIndex); +} + +void HybridState::propagateGate(Operation* gate, + const std::span targets, + const std::span ctrlsQuantum, + const std::span posCtrlsClassical, + const std::span negCtrlsClassical, + const std::span params) { + if (top) { + return; + } + + if (!isOperationExecutable(posCtrlsClassical, negCtrlsClassical)) { + return; + } + + if (!params.empty()) { + std::vector paramValues; + paramValues.reserve(params.size()); + for (Value p : params) { + if (integerValues.contains(p)) { + paramValues.push_back(static_cast(integerValues.at(p))); + } else if (doubleValues.contains(p)) { + paramValues.push_back(static_cast(doubleValues.at(p))); + } else { + llvm::report_fatal_error( + "HybridState needs a classical value for gate parameters that is " + "not existent in current HybridState."); + } + } + qState->propagateGate(gate, targets, ctrlsQuantum, paramValues); + if (qState->isTop()) { + top = true; + } + } else { + qState->propagateGate(gate, targets, ctrlsQuantum); + if (qState->isTop()) { + top = true; + } + } +} + +std::vector +HybridState::propagateMeasurement(const unsigned int quantumTarget, + const Value classicalTarget, + const std::span posCtrlsClassical, + const std::span negCtrlsClassical) { + + return propagateMeasurementOrReset(quantumTarget, false, classicalTarget, + posCtrlsClassical, negCtrlsClassical); +} + +std::vector +HybridState::propagateReset(const unsigned int target, + const std::span posCtrlsClassical, + const std::span negCtrlsClassical) { + return propagateMeasurementOrReset(target, true, nullptr, posCtrlsClassical, + negCtrlsClassical); +} + +void HybridState::propagateClassicalOperation( + Operation* op, const Value dest, const Value operand1, const Value operand2, + const Value operand3, const std::span posCtrlsClassical, + const std::span negCtrlsClassical) { + if (top || !isOperationExecutable(posCtrlsClassical, negCtrlsClassical)) { + return; + } + + if (isa(op->getResult(0).getType())) { + if (!integerValues.contains(operand1) || + (operand2 != nullptr && !integerValues.contains(operand2)) || + (operand3 != nullptr && !integerValues.contains(operand3)) || + !integerValues.contains(dest)) { + llvm::report_fatal_error( + "HybridState needs a classical value for a classical operation that " + "is not existent in current HybridState."); + } + const int64_t opRes = getArithIntegerOpResult( + op, integerValues.at(operand1), + operand2 == nullptr ? 0 : integerValues.at(operand2), + operand3 == nullptr ? 0 : integerValues.at(operand3)); + integerValues[dest] = opRes; + } else { + if (!doubleValues.contains(operand1) || + (operand2 != nullptr && !doubleValues.contains(operand2)) || + !doubleValues.contains(dest)) { + llvm::report_fatal_error( + "HybridState needs a classical value for a classical operation that " + "is not existent in current HybridState."); + } + const double opRes = getArithDoubleOpResult( + op, doubleValues.at(operand1), + operand2 == nullptr ? 0.0 : doubleValues.at(operand2)); + doubleValues[dest] = opRes; + } +} + +HybridState HybridState::unify(const HybridState& that) { + auto newHybridState = HybridState(); + newHybridState.qState = + std::make_shared(qState->unify(*that.qState)); + if (newHybridState.qState->isTop()) { + newHybridState.top = true; + return newHybridState; + } + newHybridState.probability = this->probability * that.probability; + + auto newIntegerValues = llvm::DenseMap( + integerValues.size() + that.integerValues.size()); + auto newDoubleValues = llvm::DenseMap( + doubleValues.size() + that.doubleValues.size()); + + for (const auto& [v, i] : integerValues) { + newIntegerValues[v] = i; + } + for (const auto& [v, i] : that.integerValues) { + newIntegerValues[v] = i; + } + + for (const auto& [v, d] : doubleValues) { + newDoubleValues[v] = d; + } + for (const auto& [v, d] : that.doubleValues) { + newDoubleValues[v] = d; + } + + newHybridState.integerValues = newIntegerValues; + newHybridState.doubleValues = newDoubleValues; + + return newHybridState; +} + +bool HybridState::isQubitAlwaysOne(unsigned int q) const { + return qState->isQubitAlwaysOne(q); +} + +bool HybridState::isQubitAlwaysZero(unsigned int q) const { + return qState->isQubitAlwaysZero(q); +} + +bool HybridState::isValueTrue(const Value v) const { + if (integerValues.contains(v)) { + return integerValues.at(v) != 0; + } + if (doubleValues.contains(v)) { + return std::fabs(doubleValues.at(v)) > 1e-4; + } + llvm::report_fatal_error("Value of a classical value is asked which does not " + "exist in the HybridState."); +} + +bool HybridState::hasAlwaysZeroProbability( + const std::unordered_map& qubitValues, + const llvm::DenseMap& classicalValues) const { + for (const auto& [v, i] : classicalValues) { + if (integerValues.contains(v)) { + if ((integerValues.at(v) == 0 && i) || (integerValues.at(v) != 0 && !i)) { + return true; + } + } else if (doubleValues.contains(v)) { + const bool zeroDouble = std::fabs(doubleValues.at(v)) < 1e-4; + if ((zeroDouble && i) || (!zeroDouble && !i)) { + return true; + } + } else { + llvm::report_fatal_error("Value of a classical value is asked which does " + "not exist in the HybridState."); + } + } + + return qState->hasAlwaysZeroAmplitude(qubitValues); +} + +llvm::DenseMap +HybridState::getValueThatIsEquivalentToQubit(const unsigned int qubit) const { + llvm::DenseMap result; + const bool qubitZero = qState->isQubitAlwaysZero(qubit); + const bool qubitOne = qubitZero ? false : qState->isQubitAlwaysOne(qubit); + if (!qubitZero && !qubitOne) { + return result; + } + for (const auto& [v, i] : integerValues) { + if ((qubitZero && i == 0) || (qubitOne && i != 0)) { + result[v] = true; + } else { + result[v] = false; + } + } + for (const auto& [v, d] : doubleValues) { + if ((qubitZero && std::fabs(d) < 1e-4) || + (qubitOne && std::fabs(d) >= 1e-4)) { + result[v] = true; + } else { + result[v] = false; + } + } + return result; +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp new file mode 100644 index 0000000000..67b3cec4a6 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp @@ -0,0 +1,307 @@ +/* + * 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/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp" + +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +QuantumState::QuantumState(const std::span globalQubitNumber, + const std::size_t maxNonzeroAmplitudes) + : nQubits(globalQubitNumber.size()), + maxNonzeroAmplitudes(maxNonzeroAmplitudes) { + constexpr auto maxBitNumber = sizeof(unsigned int) * 8; + if (maxBitNumber < globalQubitNumber.size()) { + // Number of qubits exceeds amount of qubits/amplitudes that can be managed + // in the union table. + top = true; + return; + } + if (maxNonzeroAmplitudes > std::numeric_limits::digits) { + // Clamp to the largest meaningful value. + this->maxNonzeroAmplitudes = std::numeric_limits::digits; + } + top = false; + std::vector qubits; + qubits.reserve(globalQubitNumber.size()); + std::ranges::copy(globalQubitNumber, std::back_inserter(qubits)); + std::ranges::sort(qubits); + unsigned int localQ = 0; + for (auto globalQ : qubits) { + globalToLocalQubitNumber[globalQ] = localQ; + ++localQ; + } + amplitudeMap[0] = std::complex(1.0, 0.0); +} + +void QuantumState::print(std::ostream& os) const { os << this->toString(); } + +std::string QuantumState::toString() const { + if (nQubits == 0) { + return ""; + } + + std::ostringstream oss; + bool first = true; + for (auto ordered = + std::map(this->amplitudeMap.begin(), this->amplitudeMap.end()); + auto const& [key, val] : ordered) { + if (!first) { + oss << ", "; + } + first = false; + + oss << "|" << qubitStringToBinary(key) << "> -> " << std::fixed + << std::setprecision(2) << val.real(); + + if (std::abs(val.imag()) > 1e-4) { + oss << (val.imag() > 0 ? " + i" : " - i"); + oss << std::fixed << std::setprecision(2) << std::abs(val.imag()); + } + } + + return oss.str(); +} + +bool QuantumState::operator==(const QuantumState& that) const { + if (this->nQubits != that.nQubits || + this->maxNonzeroAmplitudes != that.maxNonzeroAmplitudes || + this->globalToLocalQubitNumber != that.globalToLocalQubitNumber) { + return false; + } + + if (amplitudeMap.size() != that.amplitudeMap.size()) { + return false; + } + + return std::ranges::all_of( + this->amplitudeMap, + [&](const std::pair>& p) { + auto [key, val] = p; + return that.amplitudeMap.contains(key) && + abs(val - that.amplitudeMap.at(key)) < 1e-4; + }); +} + +void QuantumState::normalize() { + double denominator = 0.0; + for (const auto& value : amplitudeMap | std::views::values) { + denominator += norm(value); + } + const double invDenominator = 1 / std::sqrt(denominator); + for (const auto& key : amplitudeMap | std::views::keys) { + amplitudeMap[key] *= invDenominator; + } +} + +bool QuantumState::isTop() const { return top; } + +QuantumState QuantumState::unify(const QuantumState& that) { + // Check if future state would be too large + if (amplitudeMap.size() * that.amplitudeMap.size() > maxNonzeroAmplitudes) { + // Number of nonzero amplitudes too high. State needs to be treated as TOP. + top = true; + nQubits = 0; + maxNonzeroAmplitudes = 0; + globalToLocalQubitNumber.clear(); + amplitudeMap.clear(); + return *this; + } + + std::unordered_map newGlobalToLocalMapping; + std::unordered_map> newAmplitudes; + + // Create the new global indices + const auto globalIndicesThis = std::views::keys(globalToLocalQubitNumber); + const auto globalIndicesThat = + std::views::keys(that.globalToLocalQubitNumber); + std::vector combinedGlobalIndices; + combinedGlobalIndices.reserve(globalToLocalQubitNumber.size() + + that.globalToLocalQubitNumber.size()); + std::ranges::copy(globalIndicesThis, + std::back_inserter(combinedGlobalIndices)); + std::ranges::copy(globalIndicesThat, + std::back_inserter(combinedGlobalIndices)); + std::ranges::sort(combinedGlobalIndices); + + for (unsigned int i = 0; i < combinedGlobalIndices.size(); ++i) { + newGlobalToLocalMapping[combinedGlobalIndices[i]] = i; + } + + // Create mappings from the old to the new indices + std::unordered_map oldToNewIndicesThis; + std::unordered_map oldToNewIndicesThat; + for (const auto& keyThis : globalToLocalQubitNumber | std::views::keys) { + oldToNewIndicesThis[globalToLocalQubitNumber.at(keyThis)] = + newGlobalToLocalMapping.at(keyThis); + } + for (const auto& keyThat : that.globalToLocalQubitNumber | std::views::keys) { + oldToNewIndicesThat[that.globalToLocalQubitNumber.at(keyThat)] = + newGlobalToLocalMapping.at(keyThat); + } + + // Create new amplitude map + for (const auto& [keyThis, valThis] : amplitudeMap) { + for (const auto& [keyThat, valThat] : that.amplitudeMap) { + unsigned int currentQubitState = 0; + + for (const auto& indicesOfThis : oldToNewIndicesThis | std::views::keys) { + unsigned int bitOfQubitState = 1U << indicesOfThis; + if ((keyThis & bitOfQubitState) == bitOfQubitState) { + currentQubitState += 1U << oldToNewIndicesThis.at(indicesOfThis); + } + } + + for (const auto& indicesOfThat : oldToNewIndicesThat | std::views::keys) { + unsigned int bitOfQubitState = 1U << indicesOfThat; + if ((keyThat & bitOfQubitState) == bitOfQubitState) { + currentQubitState += 1U << oldToNewIndicesThat.at(indicesOfThat); + } + } + newAmplitudes[currentQubitState] = valThis * valThat; + } + } + auto newState = QuantumState(combinedGlobalIndices, maxNonzeroAmplitudes); + newState.amplitudeMap = newAmplitudes; + newState.globalToLocalQubitNumber = newGlobalToLocalMapping; + + return newState; +} + +void QuantumState::changeGlobalIndex(const unsigned int target, + const unsigned int newIndex) { + const auto localIndex = globalToLocalQubitNumber.at(target); + globalToLocalQubitNumber.erase(target); + globalToLocalQubitNumber[newIndex] = localIndex; +} + +void QuantumState::propagateGPhaseGate(const std::span ctrls, + const double phase) { + unsigned int ctrlMask = 0; + for (unsigned int const ctrl : ctrls) { + ctrlMask |= 1U << globalToLocalQubitNumber.at(ctrl); + } + + for (auto& [key, value] : amplitudeMap) { + if ((ctrlMask & key) != ctrlMask) { + value *= exp(Complex(0, phase)); + } + } +} + +void QuantumState::propagateGate(Operation* gate, + const std::span targets, + const std::span ctrls, + const std::span params) { + if (isa(gate)) { + propagateGPhaseGate(ctrls, *params.begin()); + return; + } + const auto gateMapping = getQubitMappingOfGates(gate, params); + + unsigned int ctrlMask = 0; + for (unsigned int const ctrl : ctrls) { + ctrlMask |= 1U << globalToLocalQubitNumber.at(ctrl); + } + std::vector localTargets; + localTargets.reserve(targets.size()); + for (unsigned int q : targets) { + localTargets.push_back(globalToLocalQubitNumber.at(q)); + } + + std::unordered_map> newValues = + getNewMappingFromQubitGate(gateMapping, localTargets, ctrlMask); + + amplitudeMap.clear(); + for (const auto& [key, value] : newValues) { + if (norm(value) > 1e-4) { + amplitudeMap[key] = value; + } + } + if (amplitudeMap.size() > maxNonzeroAmplitudes) { + // Number of nonzero amplitudes too high. State needs to be treated as TOP. + top = true; + nQubits = 0; + maxNonzeroAmplitudes = 0; + globalToLocalQubitNumber.clear(); + amplitudeMap.clear(); + } +} + +MeasurementResult QuantumState::measureQubit(const unsigned int target) { + return measureOrResetQubit(target, false); +} + +MeasurementResult QuantumState::resetQubit(const unsigned int target) { + return measureOrResetQubit(target, true); +} + +bool QuantumState::isQubitAlwaysOne(const unsigned int q) const { + if (top) { + return false; + } + const auto localIndex = globalToLocalQubitNumber.at(q); + const auto mask = 1U << localIndex; + return std::ranges::all_of( + amplitudeMap | std::views::keys, + [mask](auto qubits) { return (qubits & mask) == mask; }); +} + +bool QuantumState::isQubitAlwaysZero(const unsigned int q) const { + if (top) { + return false; + } + const auto localIndex = globalToLocalQubitNumber.at(q); + const auto mask = 1U << localIndex; + return std::ranges::all_of( + amplitudeMap | std::views::keys, + [mask](auto qubits) { return (qubits & mask) == 0; }); +} +bool QuantumState::hasAlwaysZeroAmplitude( + const std::unordered_map& qubitValues) const { + if (top) { + return false; + } + unsigned int localValue = 0; + unsigned int mask = 0; + for (const auto& [qubitIndex, qubitOne] : qubitValues) { + mask |= 1U << globalToLocalQubitNumber.at(qubitIndex); + if (qubitOne) { + localValue |= 1U << globalToLocalQubitNumber.at(qubitIndex); + } + } + return std::ranges::all_of( + amplitudeMap | std::views::keys, + [localValue, mask](auto qbit) { return (qbit & mask) != localValue; }); +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp new file mode 100644 index 0000000000..8c3a927148 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp @@ -0,0 +1,699 @@ +/* + * 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 + */ + +#ifndef MQT_CORE_UNIONTABLE +#define MQT_CORE_UNIONTABLE +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +UnionTable::UnionTable(const std::size_t maxNonzeroAmplitudes, + const std::size_t maximumHybridEntries) + : maxNonzeroAmplitudes(maxNonzeroAmplitudes), + maximumHybridEntries(maximumHybridEntries) {} + +UnionTable::~UnionTable() = default; + +void UnionTable::print(std::ostream& os) const { os << this->toString(); } + +std::string UnionTable::toString() const { + std::string result; + for (const auto& entry : entries) { + std::vector qubitIndices; + for (const auto& q : entry->participatingQubits) { + qubitIndices.push_back(qubitsToGlobalIndices.at(q)); + } + std::ranges::sort(qubitIndices, std::greater{}); + result += "Qubits: "; + for (const auto qit : qubitIndices) { + result += std::to_string(qit); + } + result += ", HybridStates: {"; + if (entry->top) { + result += "TOP"; + } else { + bool first = true; + for (HybridState const& hs : entry->states) { + if (!first) { + result += " "; + } + first = false; + result += hs.toString(); + } + } + result += "}\n"; + } + return result; +} + +void UnionTable::replaceValuesGlobally(const std::span replacedValues, + const std::span newValues) { + if (replacedValues.size() != newValues.size()) { + llvm::report_fatal_error( + "replacedValues and newValues do not have the same size."); + } + + for (unsigned int i = 0; i < replacedValues.size(); ++i) { + const auto rV = replacedValues[i]; + const auto nV = newValues[i]; + qubitsToGlobalIndices[nV] = qubitsToGlobalIndices.at(rV); + qubitsToGlobalIndices.erase(rV); + const auto ute = valuesToEntries.at(rV); + valuesToEntries.erase(rV); + valuesToEntries[nV] = ute; + if (ute->participatingQubits.contains(rV)) { + ute->participatingQubits.insert(nV); + ute->participatingQubits.erase(rV); + } else { + ute->participatingClassicalValues.insert(nV); + ute->participatingClassicalValues.erase(rV); + } + } +} + +bool UnionTable::areStatesAllTop() { + if (allTop) { + return true; + } + for (const auto& ute : entries) { + if (!ute->top) { + return false; + } + } + allTop = true; + return true; +} + +void UnionTable::propagateGate(Operation* gate, const std::span targets, + const std::span newQuantumTargets, + const std::span ctrlsQuantum, + const std::span newCtrlsQuantum, + const std::span posCtrlsClassical, + const std::span negCtrlsClassical, + const std::span params) { + // a global phase gate could only depend on classical control, but in this + // case only a global phase is added + if (targets.empty() && ctrlsQuantum.empty()) { + return; + } + const std::set participatingEntries = + collectParticipatingEntries(targets, ctrlsQuantum, posCtrlsClassical, + negCtrlsClassical, params); + + if (isa(*gate) && ctrlsQuantum.empty() && posCtrlsClassical.empty() && + negCtrlsClassical.empty() && participatingEntries.size() == 2 && + !valuesToEntries.at(targets[0])->top && + !valuesToEntries.at(targets[1])->top) { + applySwapGate(targets, newQuantumTargets); + return; + } + + const auto becameTop = unifyEntries(participatingEntries); + if (becameTop) { + replaceValuesGlobally(targets, newQuantumTargets); + replaceValuesGlobally(ctrlsQuantum, newCtrlsQuantum); + return; + } + // for GPhase, targets are not filled + const auto participatingValue = + !targets.empty() ? targets[0] : ctrlsQuantum[0]; + if (valuesToEntries.at(participatingValue)->top) { + replaceValuesGlobally(targets, newQuantumTargets); + replaceValuesGlobally(ctrlsQuantum, newCtrlsQuantum); + return; + } + + std::vector targetQubitIndices; + std::vector ctrlQubitIndices; + for (auto const q : targets) { + targetQubitIndices.push_back(qubitsToGlobalIndices.at(q)); + } + for (auto const q : ctrlsQuantum) { + ctrlQubitIndices.push_back(qubitsToGlobalIndices.at(q)); + } + + const auto ute = valuesToEntries.at(participatingValue); + for (auto& hs : ute->states) { + hs.propagateGate(gate, targetQubitIndices, ctrlQubitIndices, + posCtrlsClassical, negCtrlsClassical, params); + if (hs.isHybridStateTop()) { + putEntriesToTop({*ute}); + break; + } + } + replaceValuesGlobally(targets, newQuantumTargets); + replaceValuesGlobally(ctrlsQuantum, newCtrlsQuantum); +} + +void UnionTable::propagateClassicalOperation( + Operation* op, std::span targets, std::span results, + std::span posCtrlsClassical, std::span negCtrlsClassical) { + const auto result = results[0]; + if (!valuesToEntries.contains(result)) { + if (op->getResult(0).getType().isFloat()) { + propagateDoubleAlloc(result, 0.0); + } else { + propagateIntAlloc(result, 0); + } + } + const std::set participatingEntries = + collectParticipatingEntries(targets, results, posCtrlsClassical, + negCtrlsClassical); + + const auto becameTop = unifyEntries(participatingEntries); + if (becameTop) { + putEntriesToTop(participatingEntries); + return; + } + const Value operand1 = targets[0]; + const Value operand2 = targets.size() > 1 ? targets[1] : nullptr; + const Value operand3 = targets.size() > 2 ? targets[2] : nullptr; + + const auto ute = valuesToEntries.at(*targets.begin()); + for (auto& hs : ute->states) { + hs.propagateClassicalOperation(op, result, operand1, operand2, operand3, + posCtrlsClassical, negCtrlsClassical); + if (hs.isHybridStateTop()) { + putEntriesToTop({*ute}); + break; + } + } +} + +void UnionTable::propagateMeasurement( + const Value quantumTarget, const Value newQuantumValue, + const Value classicalTarget, const std::span posCtrlsClassical, + const std::span negCtrlsClassical) { + + if (!valuesToEntries.contains(classicalTarget)) { + propagateIntAlloc(classicalTarget, 0); + } + + std::vector targetVec = {quantumTarget, classicalTarget}; + const std::set participatingEntries = + collectParticipatingEntries(targetVec, {}, posCtrlsClassical, + negCtrlsClassical); + + std::vector quantumTargetVec = {quantumTarget}; + std::vector newQuantumValueVec = {newQuantumValue}; + const auto becameTop = unifyEntries(participatingEntries); + if (becameTop) { + replaceValuesGlobally(quantumTargetVec, newQuantumValueVec); + return; + } + + const auto ute = valuesToEntries.at(quantumTarget); + if (ute->top) { + replaceValuesGlobally(quantumTargetVec, newQuantumValueVec); + return; + } + + std::vector vecOfNewStates; + + for (auto hs : ute->states) { + auto newStates = hs.propagateMeasurement( + qubitsToGlobalIndices.at(quantumTarget), classicalTarget, + posCtrlsClassical, negCtrlsClassical); + if (hs.isHybridStateTop()) { + putEntriesToTop({*ute}); + vecOfNewStates.clear(); + break; + } + vecOfNewStates.insert(vecOfNewStates.end(), newStates.begin(), + newStates.end()); + } + if (vecOfNewStates.size() > maximumHybridEntries) { + putEntriesToTop({*ute}); + } else { + ute->states = vecOfNewStates; + } + replaceValuesGlobally(quantumTargetVec, newQuantumValueVec); +} + +void UnionTable::propagateReset(const Value quantumTarget, + const Value newQuantumValue, + const std::span posCtrlsClassical, + const std::span negCtrlsClassical) { + + std::vector quantumTargetVec = {quantumTarget}; + const std::set participatingEntries = + collectParticipatingEntries(quantumTargetVec, {}, posCtrlsClassical, + negCtrlsClassical); + + std::vector newQuantumValueVec = {newQuantumValue}; + const auto becameTop = unifyEntries(participatingEntries); + if (becameTop) { + putEntriesToTop(participatingEntries); + replaceValuesGlobally(quantumTargetVec, newQuantumValueVec); + return; + } + + const auto ute = valuesToEntries.at(quantumTarget); + if (ute->top) { + replaceValuesGlobally(quantumTargetVec, newQuantumValueVec); + return; + } + + std::vector vecOfNewStates; + + for (auto hs : ute->states) { + auto newStates = hs.propagateReset(qubitsToGlobalIndices.at(quantumTarget), + posCtrlsClassical, negCtrlsClassical); + vecOfNewStates.insert(vecOfNewStates.end(), newStates.begin(), + newStates.end()); + } + if (vecOfNewStates.size() > maximumHybridEntries) { + putEntriesToTop({*ute}); + } else { + ute->states = vecOfNewStates; + } + replaceValuesGlobally(quantumTargetVec, newQuantumValueVec); +} + +void UnionTable::propagateQubitAlloc(const Value qubit) { + unsigned int maxIndex = 0; + if (!qubitsToGlobalIndices.empty()) { + auto it = qubitsToGlobalIndices.begin(); + maxIndex = it->second; // first index present + + ++it; + for (; it != qubitsToGlobalIndices.end(); ++it) { + maxIndex = std::max(maxIndex, it->second); + } + ++maxIndex; + } + std::vector globalQubitIndex = {maxIndex}; + const auto hs = HybridState(globalQubitIndex, maxNonzeroAmplitudes); + + qubitsToGlobalIndices[qubit] = maxIndex; + auto ute = UnionTableEntry(); + ute.states.push_back(hs); + ute.participatingQubits.insert(qubit); + const auto ptrToUTE = std::make_shared(ute); + entries.insert(ptrToUTE); + valuesToEntries[qubit] = ptrToUTE; +} + +void UnionTable::propagateIntAlloc(const Value intValue, const int64_t number) { + auto hs = HybridState({}, maxNonzeroAmplitudes); + hs.addIntegerValue(intValue, number); + + auto ute = UnionTableEntry(); + ute.states.push_back(hs); + ute.participatingClassicalValues.insert(intValue); + const auto utePtr = std::make_shared(ute); + entries.insert(utePtr); + valuesToEntries[intValue] = utePtr; +} + +void UnionTable::propagateDoubleAlloc(const Value doubleValue, + const double number) { + auto hs = HybridState({}, maxNonzeroAmplitudes); + hs.addDoubleValue(doubleValue, number); + + auto ute = UnionTableEntry(); + ute.states.push_back(hs); + ute.participatingClassicalValues.insert(doubleValue); + const auto utePtr = std::make_shared(ute); + entries.insert(utePtr); + valuesToEntries[doubleValue] = utePtr; +} + +bool UnionTable::isQubitAlwaysOne(const Value q) const { + const unsigned int qubitIndex = qubitsToGlobalIndices.at(q); + const auto ute = valuesToEntries.at(q); + if (ute->top) { + return false; + } + return std::ranges::all_of(ute->states, [&](const auto& hs) { + return hs.isQubitAlwaysOne(qubitIndex); + }); +} + +bool UnionTable::isQubitAlwaysZero(const Value q) const { + const unsigned int qubitIndex = qubitsToGlobalIndices.at(q); + const auto ute = valuesToEntries.at(q); + if (ute->top) { + return false; + } + return std::ranges::all_of(ute->states, [&](const auto& hs) { + return hs.isQubitAlwaysZero(qubitIndex); + }); +} + +bool UnionTable::isClassicalValueAlwaysTrue(const Value c) const { + const auto ute = valuesToEntries.at(c); + if (ute->top) { + return false; + } + return std::ranges::all_of(ute->states, + [&](const auto& hs) { return hs.isValueTrue(c); }); +} + +bool UnionTable::isClassicalValueAlwaysFalse(const Value c) const { + const auto ute = valuesToEntries.at(c); + if (ute->top) { + return false; + } + return std::ranges::all_of( + ute->states, [&](const auto& hs) { return !hs.isValueTrue(c); }); +} + +bool UnionTable::hasAlwaysZeroProbability( + const llvm::DenseMap& qubitValues, + const llvm::DenseMap& classicalValues) const { + std::set participatingEntries; + for (const auto& [qV, _] : qubitValues) { + participatingEntries.insert(*valuesToEntries.at(qV)); + } + for (const auto& [cV, _] : classicalValues) { + participatingEntries.insert(*valuesToEntries.at(cV)); + } + for (const auto& ute : participatingEntries) { + if (ute.top) { + return false; + } + std::unordered_map qubitValuesThisEntry; + llvm::DenseMap classicalValuesThisEntry; + for (const auto& [qV, qBool] : qubitValues) { + if (ute.participatingQubits.contains(qV)) { + qubitValuesThisEntry[qubitsToGlobalIndices.at(qV)] = qBool; + } + } + for (const auto& [v, vBool] : classicalValues) { + if (ute.participatingClassicalValues.contains(v)) { + classicalValuesThisEntry[v] = vBool; + } + } + bool oneEntryIsNonzero = false; + for (const auto& hs : ute.states) { + if (!hs.hasAlwaysZeroProbability(qubitValuesThisEntry, + classicalValuesThisEntry)) { + oneEntryIsNonzero = true; + break; + } + } + if (!oneEntryIsNonzero) { + return true; + } + } + return false; +} + +llvm::DenseMap +UnionTable::getValueThatIsEquivalentToQubit(const Value qubit) const { + const auto uteOfQubit = valuesToEntries.at(qubit); + const auto indexOfQubit = qubitsToGlobalIndices.at(qubit); + llvm::DenseMap result; + bool found = false; + bool alwaysOne = true; + bool alwaysZero = true; + for (const auto& hs : uteOfQubit->states) { + alwaysOne &= hs.isQubitAlwaysOne(indexOfQubit); + alwaysZero &= hs.isQubitAlwaysZero(indexOfQubit); + auto currentResult = hs.getValueThatIsEquivalentToQubit(indexOfQubit); + if (currentResult.empty() || (result.empty() && found)) { + result.clear(); + continue; + } + if (!found) { + result = currentResult; + found = true; + } else { + for (const auto& [value, inverse] : currentResult) { + if (!result.contains(value) || result.at(value) != inverse) { + result.erase(value); + } + } + } + } + if (alwaysOne) { + auto valuesThatAreAlwaysTrue = getClassicalValuesThatAreAlwaysTrueOrFalse(); + result.reserve(valuesThatAreAlwaysTrue.size()); + for (const auto& [k, v] : valuesThatAreAlwaysTrue) { + result[k] = v; + } + } else if (alwaysZero) { + auto valuesThatAreAlwaysTrue = getClassicalValuesThatAreAlwaysTrueOrFalse(); + result.reserve(valuesThatAreAlwaysTrue.size()); + for (const auto& [k, v] : valuesThatAreAlwaysTrue) { + result[k] = !v; + } + } + return result; +} + +std::optional +UnionTable::globalPhaseThatIsAdded(Operation* op, const Value target, + const std::span ctrlsQuantum, + const std::span posCtrlsClassical, + const std::span negCtrlsClassical) { + // Diagonal gates w/o parameters: IdOp, ZOp, SOp, SdgOp, TOp, TdgOp + if (isa(op)) { + return {0.0}; + } + if (!(isa(op) || isa(op) || isa(op) || isa(op) || + isa(op))) { + return {}; + } + const auto targetIndex = qubitsToGlobalIndices.at(target); + const auto targetUte = valuesToEntries.at(target); + + if (targetUte->top) { + return {}; + } + + bool alwaysOne = true; + bool alwaysZero = true; + + for (const auto& hs : targetUte->states) { + alwaysOne &= hs.isQubitAlwaysOne(targetIndex); + alwaysZero &= hs.isQubitAlwaysZero(targetIndex); + if (!alwaysOne && !alwaysZero) { + break; + } + } + + if (alwaysZero) { + return {0.0}; + } + if (!alwaysOne && ctrlsQuantum.empty() && posCtrlsClassical.empty() && + negCtrlsClassical.empty()) { + return {}; + } + + const auto participatingEntries = collectParticipatingEntries( + {}, ctrlsQuantum, posCtrlsClassical, negCtrlsClassical, {}); + + // Check if state |11...11> can be reached + bool highestStateReachable = alwaysOne; + bool highestStateAlwaysReached = alwaysOne; + for (const auto& ute : participatingEntries) { + if (ute.top) { + return {}; + } + std::unordered_map qubitCtrlThisEntry; + llvm::DenseMap classicalCtrlThisEntry; + for (const auto q : ctrlsQuantum) { + if (ute.participatingQubits.contains(q)) { + qubitCtrlThisEntry[qubitsToGlobalIndices.at(q)] = true; + } + } + for (const auto c : posCtrlsClassical) { + if (ute.participatingClassicalValues.contains(c)) { + classicalCtrlThisEntry[c] = true; + } + } + for (const auto c : negCtrlsClassical) { + if (ute.participatingClassicalValues.contains(c)) { + classicalCtrlThisEntry[c] = false; + } + } + for (const auto& hs : ute.states) { + const auto ctrlsZeroProbability = hs.hasAlwaysZeroProbability( + qubitCtrlThisEntry, classicalCtrlThisEntry); + highestStateReachable |= !ctrlsZeroProbability; + highestStateAlwaysReached &= !ctrlsZeroProbability; + } + if (highestStateReachable && !highestStateAlwaysReached) { + return {}; + } + } + if (!highestStateReachable) { + return {0.0}; + } + if (!highestStateAlwaysReached) { + return {}; + } + + // Only highest state reachable, return respective phase + if (isa(op)) { + return {std::numbers::pi}; + } + if (isa(op)) { + return {std::numbers::pi / 2}; + } + if (isa(op)) { + return {3.0 * std::numbers::pi / 2}; + } + if (isa(op)) { + return {std::numbers::pi / 4}; + } + // Tdg Op + return {-std::numbers::pi / 4}; +} + +SuperfluousResult +UnionTable::getSuperfluousControls(const std::span qubitCtrls, + const std::span posCtrlsClassical, + const std::span negCtrlsClassical) { + SuperfluousResult res; + for (const auto& qCtrl : qubitCtrls) { + const auto qIndex = qubitsToGlobalIndices.at(qCtrl); + bool alwaysOne = true; + bool alwaysZero = true; + if (valuesToEntries.at(qCtrl)->top) { + alwaysOne = false; + alwaysZero = false; + } + for (const auto& hs : valuesToEntries.at(qCtrl)->states) { + if (hs.isHybridStateTop()) { + alwaysOne = false; + alwaysZero = false; + break; + } + if (alwaysZero && !hs.isQubitAlwaysZero(qIndex)) { + alwaysZero = false; + } + alwaysOne &= !alwaysZero && hs.isQubitAlwaysOne(qIndex); + } + if (alwaysZero) { + res.completelySuperfluous = true; + return res; + } + if (alwaysOne) { + res.superfluousQubits.insert(qCtrl); + } + } + for (const auto& posCtrl : posCtrlsClassical) { + bool alwaysTrue = true; + bool alwaysFalse = true; + for (const auto& hs : valuesToEntries.at(posCtrl)->states) { + if (!alwaysTrue && !alwaysFalse) { + break; + } + const bool valueTrue = hs.isValueTrue(posCtrl); + alwaysTrue &= valueTrue; + alwaysFalse &= !valueTrue; + } + if (alwaysFalse) { + res.completelySuperfluous = true; + return res; + } + if (alwaysTrue) { + res.superfluousClassicalValues.insert(posCtrl); + } + } + for (const auto& negCtrl : negCtrlsClassical) { + bool alwaysTrue = true; + bool alwaysFalse = true; + for (const auto& hs : valuesToEntries.at(negCtrl)->states) { + if (!alwaysTrue && !alwaysFalse) { + break; + } + const bool valueTrue = hs.isValueTrue(negCtrl); + alwaysTrue &= valueTrue; + alwaysFalse &= !valueTrue; + } + if (alwaysTrue) { + res.completelySuperfluous = true; + return res; + } + if (alwaysFalse) { + res.superfluousClassicalValues.insert(negCtrl); + } + } + return res; +} + +bool UnionTable::areThereSatisfiableCombinations( + const std::span qubitCtrls, const std::span posCtrlsClassical, + const std::span negCtrlsClassical) const { + llvm::DenseMap qubitValues; + llvm::DenseMap classicalValues; + for (const auto& q : qubitCtrls) { + qubitValues[q] = true; + } + for (const auto& v : posCtrlsClassical) { + classicalValues[v] = true; + } + for (const auto& v : negCtrlsClassical) { + classicalValues[v] = false; + } + return !hasAlwaysZeroProbability(qubitValues, classicalValues); +} + +bool UnionTable::isQubitImplied( + const Value q, const std::span qubits, + const std::span classicalPositive, + const std::span classicalNegative) const { + llvm::DenseMap qMap; + llvm::DenseMap cPMap; + qMap[q] = false; + for (const auto& qV : qubits) { + qMap[qV] = true; + if (hasAlwaysZeroProbability(qMap, cPMap)) { + return true; + } + qMap.erase(qV); + } + for (const auto& cP : classicalPositive) { + cPMap[cP] = true; + if (hasAlwaysZeroProbability(qMap, cPMap)) { + return true; + } + cPMap.erase(cP); + } + for (const auto& cP : classicalNegative) { + cPMap[cP] = false; + if (hasAlwaysZeroProbability(qMap, cPMap)) { + return true; + } + cPMap.erase(cP); + } + return false; +} + +} // namespace mlir::qco + +#endif // MQT_CORE_UNIONTABLE diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt index 46295f85ea..4357cd4219 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt @@ -9,6 +9,10 @@ set(target_name mqt-core-mlir-unittest-optimizations) add_executable( ${target_name} + ConstantPropagation/test_hybridState.cpp + ConstantPropagation/test_quantumState.cpp + ConstantPropagation/test_unionTable.cpp + test_qco_constant_propagation.cpp test_qco_hadamard_lifting.cpp test_qco_measurement_lifting.cpp test_qco_merge_single_qubit_rotation.cpp @@ -20,6 +24,7 @@ add_executable( target_link_libraries( ${target_name} PRIVATE GTest::gtest_main + GTest::gmock MLIRControlFlowDialect MLIRQCODDFunctionality MLIRQCOProgramBuilder diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp new file mode 100644 index 0000000000..717ec1c8f1 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp @@ -0,0 +1,776 @@ +/* + * 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/Optimizations/ConstantPropagation/HybridState.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +using namespace mlir::qco; + +class HybridStateTest : public testing::Test { +protected: + mlir::MLIRContext context; + QCOProgramBuilder programBuilder; + QCOProgramBuilder referenceBuilder; + std::vector fourQubits = {0, 1, 2, 3}; + std::vector vectorZero = {0}; + std::vector vectorOne = {1}; + std::vector vectorTwo = {2}; + std::vector vectorThree = {3}; + std::vector vectorFour = {4}; + std::vector vectorZeroOne = {0, 1}; + std::vector vectorOneThree = {1, 3}; + std::vector vectorTwoOne = {2, 1}; + std::vector vectorZeroTwoFour = {0, 2, 4}; + + HOp hOp; + XOp xOp; + ZOp zOp; + SOp sOp; + UOp uOp; + + mlir::Value v1; + mlir::Value v2; + mlir::Value v3; + mlir::Value v4; + + HybridStateTest() : programBuilder(&context), referenceBuilder(&context) {} + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.initialize(); + referenceBuilder.initialize(); + + auto q = programBuilder.allocQubitRegister(4); + hOp = HOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + xOp = XOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + zOp = ZOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + sOp = SOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + uOp = UOp::create(programBuilder, programBuilder.getLoc(), {q[0].getType()}, + {q[0], q[1], q[2], q[3]}); + v1 = q[0]; + v2 = q[1]; + v3 = q[2]; + v4 = q[3]; + } +}; + +TEST_F(HybridStateTest, ApplyHGate) { + auto hState = HybridState(vectorZero, 4); + hState.propagateGate(hOp.getOperation(), vectorZero); + + EXPECT_THAT(hState.toString(), + testing::HasSubstr("{|0> -> 0.71, |1> -> 0.71}: p = 1.00;")); +} + +TEST_F(HybridStateTest, ApplyHGateToThirdQubit) { + auto hState = HybridState(fourQubits, 4, 0.5); + hState.propagateGate(hOp.getOperation(), vectorTwo); + + EXPECT_THAT( + hState.toString(), + testing::HasSubstr("{|0000> -> 0.71, |0100> -> 0.71}: p = 0.50;")); +} + +TEST_F(HybridStateTest, ApplyParametrizedGateToThirdQubit) { + std::vector params = {v1, v2, v3}; + auto hState = HybridState(fourQubits, 4); + hState.addIntegerValue(v1, 1); + hState.addDoubleValue(v2, 0.5); + hState.addDoubleValue(v3, 2.0); + hState.propagateGate(hOp.getOperation(), vectorTwo); + hState.propagateGate(uOp.getOperation(), vectorTwo, {}, {}, {}, params); + + const auto resStr = hState.toString(); + EXPECT_THAT(resStr, testing::HasSubstr( + "{|0000> -> 0.76 - i0.31, |0100> -> -0.20 + i0.53}")); + EXPECT_THAT(resStr, testing::ContainsRegex("integerValue0 = 1")); + EXPECT_THAT(resStr, + testing::AnyOf(testing::HasSubstr("doubleValue0 = 2.00"), + testing::HasSubstr("doubleValue1 = 2.00"))); + EXPECT_THAT(resStr, + testing::AnyOf(testing::HasSubstr("doubleValue0 = 0.50"), + testing::HasSubstr("doubleValue1 = 0.50"))); +} + +TEST_F(HybridStateTest, ApplyQuantumControlledGate) { + auto hState = HybridState(fourQubits, 4); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne); + + EXPECT_THAT(hState.toString(), + testing::HasSubstr("{|0010> -> 0.71, |1000> -> 0.71}")); +} + +TEST_F(HybridStateTest, ApplyClassicalControlledGateThatsFalse) { + auto hState = HybridState(fourQubits, 4); + std::vector ctrl = {v1}; + hState.addIntegerValue(v1, 0); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne, ctrl); + + EXPECT_THAT( + hState.toString(), + testing::HasSubstr( + "{|1000> -> 0.71, |1010> -> 0.71}: integerValue0 = 0; p = 1.00")); +} + +TEST_F(HybridStateTest, ApplyClassicalControlledGateThatsTrue) { + auto hState = HybridState(fourQubits, 4); + std::vector ctrl = {v1}; + hState.addIntegerValue(v1, 1); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne, ctrl); + + EXPECT_THAT( + hState.toString(), + testing::HasSubstr( + "{|0010> -> 0.71, |1000> -> 0.71}: integerValue0 = 1; p = 1.00")); +} + +TEST_F(HybridStateTest, ApplyNegClassicalControlledGateThatsFalse) { + auto hState = HybridState(fourQubits, 4); + v1 = mlir::Value(); + std::vector ctrl = {v1}; + hState.addIntegerValue(v1, 0); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne, {}, ctrl); + + EXPECT_THAT( + hState.toString(), + testing::HasSubstr( + "{|0010> -> 0.71, |1000> -> 0.71}: integerValue0 = 0; p = 1.00")); +} + +TEST_F(HybridStateTest, ApplyTwoTimesClassicalControlledGate) { + auto hState = HybridState(fourQubits, 4); + std::vector ctrls = {v1, v2}; + hState.addIntegerValue(v1, 1); + hState.addIntegerValue(v2, 3); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne, ctrls); + + const auto resStr = hState.toString(); + EXPECT_THAT(resStr, testing::HasSubstr("{|0010> -> 0.71, |1000> -> 0.71}: ")); + EXPECT_THAT(resStr, testing::HasSubstr("; p = 1.00")); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 3"), + testing::HasSubstr("integerValue1 = 3"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 1"), + testing::HasSubstr("integerValue1 = 1"))); +} + +TEST_F(HybridStateTest, ApplyClassicalDoubleControlledGateThatsFalse) { + auto hState = HybridState(fourQubits, 4); + std::vector ctrl = {v1}; + hState.addDoubleValue(v1, 0.0); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne, ctrl); + + EXPECT_THAT( + hState.toString(), + testing::HasSubstr( + "{|1000> -> 0.71, |1010> -> 0.71}: doubleValue0 = 0.00; p = 1.00")); +} + +TEST_F(HybridStateTest, ApplyClassicalDoubleControlledGateThatsTrue) { + auto hState = HybridState(fourQubits, 4); + std::vector ctrl = {v1}; + hState.addDoubleValue(v1, 1.1); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne, ctrl); + + EXPECT_THAT( + hState.toString(), + testing::HasSubstr( + "{|0010> -> 0.71, |1000> -> 0.71}: doubleValue0 = 1.10; p = 1.00")); +} + +TEST_F(HybridStateTest, ApplyNegClassicalDoubleControlledGateThatsFalse) { + auto hState = HybridState(fourQubits, 4); + constexpr auto v1 = mlir::Value(); + std::vector ctrl = {v1}; + hState.addDoubleValue(v1, 0.0); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne, {}, ctrl); + + EXPECT_THAT( + hState.toString(), + testing::HasSubstr( + "{|0010> -> 0.71, |1000> -> 0.71}: doubleValue0 = 0.00; p = 1.00")); +} + +TEST_F(HybridStateTest, ApplyTwoTimesClassicalDoubleControlledGate) { + auto hState = HybridState(fourQubits, 4); + std::vector ctrls = {v1, v2}; + hState.addDoubleValue(v1, 1.2); + hState.addDoubleValue(v2, 3.2); + hState.propagateGate(xOp.getOperation(), vectorThree); + hState.propagateGate(hOp.getOperation(), vectorOne); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorOne, ctrls); + + const auto resStr = hState.toString(); + EXPECT_THAT(resStr, testing::HasSubstr("{|0010> -> 0.71, |1000> -> 0.71}: ")); + EXPECT_THAT(resStr, testing::HasSubstr("; p = 1.00")); + EXPECT_THAT(resStr, + testing::AnyOf(testing::HasSubstr("doubleValue0 = 3.20"), + testing::HasSubstr("doubleValue1 = 3.20"))); + EXPECT_THAT(resStr, + testing::AnyOf(testing::HasSubstr("doubleValue0 = 1.20"), + testing::HasSubstr("doubleValue1 = 1.20"))); +} + +TEST_F(HybridStateTest, handleErrorIfTooManyAmplitudesAreNonzero) { + auto hState = HybridState(fourQubits, 2); + hState.propagateGate(hOp.getOperation(), vectorThree); + hState.propagateGate(xOp.getOperation(), vectorTwo, vectorThree); + // Error occurs here + hState.propagateGate(hOp.getOperation(), vectorTwo); + // Should leave state in TOP + hState.propagateGate(sOp.getOperation(), vectorZero); + + EXPECT_TRUE(hState.isHybridStateTop()); +} + +TEST_F(HybridStateTest, doMeasurementWithOneResult) { + auto hState = HybridState(fourQubits, 4, 0.6); + hState.addIntegerValue(v1, 0); + hState.propagateGate(xOp.getOperation(), vectorZero); + const auto resStates = hState.propagateMeasurement(0, v1); + + EXPECT_TRUE(resStates.size() == 1); + const auto& resHybridState = resStates.at(0); + EXPECT_THAT( + resHybridState.toString(), + testing::HasSubstr("{|0001> -> 1.00}: integerValue0 = 1; p = 0.60")); +} + +TEST_F(HybridStateTest, doMeasurementWithTwoResults) { + auto hState = HybridState(fourQubits, 4, 0.6); + hState.addIntegerValue(v1, 10); + hState.propagateGate(hOp.getOperation(), vectorZero); + hState.propagateGate(xOp.getOperation(), vectorTwo, vectorZero); + const auto resStates = hState.propagateMeasurement(0, v1); + + EXPECT_TRUE(resStates.size() == 2); + const auto resStrings = + resStates.at(0).toString() + resStates.at(1).toString(); + EXPECT_THAT(resStrings, testing::HasSubstr( + "{|0000> -> 1.00}: integerValue0 = 0; p = 0.30")); + EXPECT_THAT(resStrings, testing::HasSubstr( + "{|0101> -> 1.00}: integerValue0 = 1; p = 0.30")); +} + +TEST_F(HybridStateTest, doNotMeasureAsClassicalCtrlIsNegative) { + auto hState = HybridState(fourQubits, 4, 0.6); + constexpr auto v1 = mlir::Value(); + hState.addIntegerValue(v1, 0); + std::vector ctrl = {v1}; + hState.propagateGate(hOp.getOperation(), vectorZero); + const auto resStates = hState.propagateMeasurement(0, v1, ctrl); + + EXPECT_TRUE(resStates.size() == 1); + const auto& resHybridState = resStates.at(0); + EXPECT_THAT( + resHybridState.toString(), + testing::HasSubstr( + "{|0000> -> 0.71, |0001> -> 0.71}: integerValue0 = 0; p = 0.60")); +} + +TEST_F(HybridStateTest, doMeasurementWithPosNegClassicalCtrl) { + auto hState = HybridState(fourQubits, 4, 0.6); + constexpr auto v1 = mlir::Value(); + hState.addIntegerValue(v1, 3); + std::vector ctrl = {v1}; + hState.propagateGate(hOp.getOperation(), vectorZero); + const auto resStates = hState.propagateMeasurement(0, v1, {}, ctrl); + + EXPECT_TRUE(resStates.size() == 1); + const auto& resHybridState = resStates.at(0); + EXPECT_THAT( + resHybridState.toString(), + testing::HasSubstr( + "{|0000> -> 0.71, |0001> -> 0.71}: integerValue0 = 3; p = 0.60")); +} + +TEST_F(HybridStateTest, doResetWithOneResult) { + auto hState = HybridState(vectorZero, 2); + hState.addIntegerValue(v1, 3); + std::vector ctrl = {v1}; + hState.propagateGate(xOp.getOperation(), vectorZero); + + const auto resStates = hState.propagateReset(0, ctrl); + + EXPECT_TRUE(resStates.size() == 1); + const auto& resHybridState = resStates.at(0); + EXPECT_THAT(resHybridState.toString(), + testing::HasSubstr("{|0> -> 1.00}: integerValue0 = 3; p = 1.00")); +} + +TEST_F(HybridStateTest, doResetWithTwoResults) { + auto hState = HybridState(fourQubits, 2, 0.6); + hState.addIntegerValue(v1, 0); + std::vector ctrl = {v1}; + hState.propagateGate(hOp.getOperation(), vectorZero); + hState.propagateGate(xOp.getOperation(), vectorThree, vectorZero); + + const auto resStates = hState.propagateReset(0, {}, ctrl); + + EXPECT_TRUE(resStates.size() == 2); + const auto resString = + resStates.at(0).toString() + resStates.at(1).toString(); + EXPECT_THAT(resString, testing::HasSubstr( + "{|0000> -> 1.00}: integerValue0 = 0; p = 0.30")); + EXPECT_THAT(resString, testing::HasSubstr( + "{|1000> -> 1.00}: integerValue0 = 0; p = 0.30")); +} + +TEST_F(HybridStateTest, doNotResetAsClassicalCtrlIsNegative) { + auto hState = HybridState(fourQubits, 4, 0.6); + constexpr auto v1 = mlir::Value(); + hState.addIntegerValue(v1, 0); + std::vector ctrl = {v1}; + hState.propagateGate(hOp.getOperation(), vectorZero); + const auto resStates = hState.propagateReset(0, ctrl); + + EXPECT_TRUE(resStates.size() == 1); + const auto& resHybridState = resStates.at(0); + EXPECT_THAT( + resHybridState.toString(), + testing::HasSubstr( + "{|0000> -> 0.71, |0001> -> 0.71}: integerValue0 = 0; p = 0.60")); +} + +TEST_F(HybridStateTest, doResetWithPosNegClassicalCtrl) { + auto hState = HybridState(fourQubits, 4, 0.6); + constexpr auto v1 = mlir::Value(); + hState.addIntegerValue(v1, 3); + std::vector ctrl = {v1}; + hState.propagateGate(hOp.getOperation(), vectorZero); + const auto resStates = hState.propagateReset(0, {}, ctrl); + + EXPECT_TRUE(resStates.size() == 1); + const auto& resHybridState = resStates.at(0); + EXPECT_THAT( + resHybridState.toString(), + testing::HasSubstr( + "{|0000> -> 0.71, |0001> -> 0.71}: integerValue0 = 3; p = 0.60")); +} + +TEST_F(HybridStateTest, doMeasurementOnTop) { + auto hState = HybridState(fourQubits, 2); + constexpr auto v1 = mlir::Value(); + hState.propagateGate(hOp.getOperation(), vectorThree); + hState.propagateGate(xOp.getOperation(), vectorTwo, vectorThree); + // Error occurs here + hState.propagateGate(hOp.getOperation(), vectorTwo); + // Should leave state in TOP + hState.addIntegerValue(v1, 3); + hState.propagateMeasurement(0, v1); + + EXPECT_TRUE(hState.isHybridStateTop()); +} + +TEST_F(HybridStateTest, doResetOnTop) { + auto hState = HybridState(fourQubits, 2); + hState.propagateGate(hOp.getOperation(), vectorThree); + hState.propagateGate(xOp.getOperation(), vectorTwo, vectorThree); + // Error occurs here + hState.propagateGate(hOp.getOperation(), vectorTwo); + // Should leave state in TOP + hState.addIntegerValue(v1, 3); + hState.propagateReset(0); + + EXPECT_TRUE(hState.isHybridStateTop()); +} + +TEST_F(HybridStateTest, unifyTwoHybridStates) { + + auto hState1 = HybridState(vectorZeroTwoFour, 10, 0.8); + hState1.propagateGate(hOp.getOperation(), vectorFour); + hState1.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + hState1.propagateGate(xOp.getOperation(), vectorZero, vectorTwo); + hState1.addIntegerValue(v1, 4); + + auto hState2 = HybridState(vectorOneThree, 10, 0.5); + hState2.propagateGate(hOp.getOperation(), vectorThree); + hState2.propagateGate(xOp.getOperation(), vectorOne, vectorThree); + hState2.addIntegerValue(v2, 7); + hState2.addDoubleValue(v3, 4.2); + + const HybridState unified = hState1.unify(hState2); + const auto resStr = unified.toString(); + + EXPECT_THAT(resStr, + testing::HasSubstr("{|00000> -> 0.50, |01010> -> 0.50, " + "|10101> -> 0.50, |11111> -> 0.50}: ")); + EXPECT_THAT(resStr, testing::HasSubstr("doubleValue0 = 4.20; p = 0.40")); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 4"), + testing::HasSubstr("integerValue1 = 4"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 7"), + testing::HasSubstr("integerValue1 = 7"))); +} + +TEST_F(HybridStateTest, unifyHybridStatesOneWithoutQuantum) { + auto hState1 = HybridState(fourQubits, 10, 0.8); + hState1.propagateGate(hOp.getOperation(), vectorOne); + hState1.addIntegerValue(v1, 4); + + auto hState2 = HybridState({}, 10, 0.5); + hState2.addIntegerValue(v2, 7); + hState2.addDoubleValue(v3, 4.2); + + const HybridState unified = hState1.unify(hState2); + const auto resStr = unified.toString(); + + EXPECT_THAT(resStr, testing::HasSubstr("{|0000> -> 0.71, |0010> -> 0.71}: ")); + EXPECT_THAT(resStr, testing::HasSubstr(", doubleValue0 = 4.20; p = 0.40")); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 7"), + testing::HasSubstr("integerValue1 = 7"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 4"), + testing::HasSubstr("integerValue1 = 4"))); +} + +TEST_F(HybridStateTest, unifyHybridStatesWithoutQuantum) { + auto hState1 = HybridState({}, 10, 0.8); + hState1.addIntegerValue(v1, 4); + + auto hState2 = HybridState({}, 10, 0.5); + hState2.addIntegerValue(v2, 7); + hState2.addDoubleValue(v3, 4.2); + + const HybridState unified = hState1.unify(hState2); + const auto resStr = unified.toString(); + + EXPECT_THAT(resStr, testing::HasSubstr("{}: ")); + EXPECT_THAT(resStr, testing::HasSubstr(", doubleValue0 = 4.20; p = 0.40")); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 4"), + testing::HasSubstr("integerValue1 = 4"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 7"), + testing::HasSubstr("integerValue1 = 7"))); +} + +TEST_F(HybridStateTest, unifyTooLargeHybridStates) { + auto hState1 = HybridState(vectorZeroTwoFour, 3); + hState1.propagateGate(hOp.getOperation(), vectorFour); + hState1.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + hState1.propagateGate(xOp.getOperation(), vectorZero, vectorTwo); + + auto hState2 = HybridState(vectorOneThree, 3); + hState2.propagateGate(hOp.getOperation(), vectorThree); + hState2.propagateGate(xOp.getOperation(), vectorOne, vectorThree); + + const auto hs = hState1.unify(hState2); + + EXPECT_TRUE(hs.isHybridStateTop()); +} + +TEST_F(HybridStateTest, intOpTwoValueOperation) { + auto hState = HybridState(fourQubits, 3); + const auto i1 = programBuilder.getI64IntegerAttr(3); + const auto i2 = programBuilder.getI64IntegerAttr(9); + const auto i3 = programBuilder.getI64IntegerAttr(0); + + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + const mlir::Value val3 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i3); + + hState.addIntegerValue(val1, 3); + hState.addIntegerValue(val2, 9); + hState.addIntegerValue(val3, 0); + + const auto subIOp = mlir::arith::SubIOp::create( + programBuilder, programBuilder.getLoc(), val3.getType(), val1, val2); + + hState.propagateClassicalOperation(subIOp, val3, val1, val2); + + const auto resStr = hState.toString(); + + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 3"), + testing::HasSubstr("integerValue1 = 3"), + testing::HasSubstr("integerValue2 = 3"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 9"), + testing::HasSubstr("integerValue1 = 9"), + testing::HasSubstr("integerValue2 = 9"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = -6"), + testing::HasSubstr("integerValue1 = -6"), + testing::HasSubstr("integerValue2 = -6"))); +} + +TEST_F(HybridStateTest, intOpThreeValueOperation) { + auto hState = HybridState(fourQubits, 3); + const auto i0 = programBuilder.getI64IntegerAttr(0); + const auto i1 = programBuilder.getI64IntegerAttr(3); + const auto i2 = programBuilder.getI64IntegerAttr(9); + const auto i3 = programBuilder.getI64IntegerAttr(0); + + const mlir::Value val0 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i0); + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + const mlir::Value val3 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i3); + + hState.addIntegerValue(val0, 0); + hState.addIntegerValue(val1, 3); + hState.addIntegerValue(val2, 9); + hState.addIntegerValue(val3, 1); + + const auto selectOp = + mlir::arith::SelectOp::create(programBuilder, programBuilder.getLoc(), + val3.getType(), val0, val1, val2); + const auto subIOp = mlir::arith::SubIOp::create( + programBuilder, programBuilder.getLoc(), val3.getType(), val3, val1); + + hState.propagateClassicalOperation(selectOp, val3, val0, val1, val2); + hState.propagateClassicalOperation(subIOp, val3, val3, val1); + + const auto resStr = hState.toString(); + + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 0"), + testing::HasSubstr("integerValue1 = 0"), + testing::HasSubstr("integerValue2 = 0"), + testing::HasSubstr("integerValue3 = 0"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 3"), + testing::HasSubstr("integerValue1 = 3"), + testing::HasSubstr("integerValue2 = 3"), + testing::HasSubstr("integerValue3 = 3"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 9"), + testing::HasSubstr("integerValue1 = 9"), + testing::HasSubstr("integerValue2 = 9"), + testing::HasSubstr("integerValue3 = 9"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("integerValue0 = 6"), + testing::HasSubstr("integerValue1 = 6"), + testing::HasSubstr("integerValue2 = 6"), + testing::HasSubstr("integerValue3 = 6"))); +} + +TEST_F(HybridStateTest, doubleOpOneValueOperation) { + auto hState = HybridState(fourQubits, 3); + const auto i1 = programBuilder.getF64FloatAttr(-2.7); + const auto i2 = programBuilder.getF64FloatAttr(0); + + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + + hState.addDoubleValue(val1, -2.7); + hState.addDoubleValue(val2, 0); + + const auto negFOp = mlir::arith::NegFOp::create( + programBuilder, programBuilder.getLoc(), val2.getType(), val1); + + hState.propagateClassicalOperation(negFOp, val2, val1); + + const auto resStr = hState.toString(); + + EXPECT_THAT(resStr, + testing::AnyOf(testing::HasSubstr("doubleValue0 = -2.7"), + testing::HasSubstr("doubleValue1 = -2.7"))); + EXPECT_THAT(resStr, testing::AnyOf(testing::HasSubstr("doubleValue0 = 2.7"), + testing::HasSubstr("doubleValue1 = 2.7"))); +} + +TEST_F(HybridStateTest, doubleOpTwoValueOperation) { + auto hState = HybridState(fourQubits, 3); + const auto i1 = programBuilder.getF64FloatAttr(-2.5); + const auto i2 = programBuilder.getF64FloatAttr(1.3); + + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + + hState.addDoubleValue(val1, -2.5); + hState.addDoubleValue(val2, 1.3); + + const auto mulFOp = mlir::arith::MulFOp::create( + programBuilder, programBuilder.getLoc(), val1, val2); + + hState.propagateClassicalOperation(mulFOp, val2, val1, val2); + + const auto resStr = hState.toString(); + + EXPECT_THAT(resStr, + testing::AnyOf(testing::HasSubstr("doubleValue0 = -2.50"), + testing::HasSubstr("doubleValue1 = -2.50"))); + EXPECT_THAT(resStr, + testing::AnyOf(testing::HasSubstr("doubleValue0 = -3.25"), + testing::HasSubstr("doubleValue1 = -3.25"))); +} + +TEST_F(HybridStateTest, isDoubleValueTrue) { + auto hState = HybridState(fourQubits, 3); + const auto i1 = programBuilder.getF64FloatAttr(-2.5); + const auto i2 = programBuilder.getF64FloatAttr(0.0); + + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + + hState.addDoubleValue(val1, -2.5); + hState.addDoubleValue(val2, 0.0); + + EXPECT_TRUE(hState.isValueTrue(val1)); + EXPECT_FALSE(hState.isValueTrue(val2)); +} + +TEST_F(HybridStateTest, hybridStatesEqual) { + auto hState1 = HybridState(vectorZeroTwoFour, 3); + auto hState2 = HybridState(vectorZeroTwoFour, 3); + const auto i0 = programBuilder.getI64IntegerAttr(0); + const auto i1 = programBuilder.getF64FloatAttr(-2.5); + const auto i2 = programBuilder.getF64FloatAttr(0.0); + + const mlir::Value val0 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i0); + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + + hState1.addIntegerValue(val0, 9); + hState1.addDoubleValue(val1, -2.5); + hState1.addDoubleValue(val2, 0.0); + hState1.propagateGate(hOp.getOperation(), vectorFour); + hState1.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + + hState2.addIntegerValue(val0, 9); + hState2.addDoubleValue(val1, -2.5); + hState2.addDoubleValue(val2, 0.0); + hState2.propagateGate(hOp.getOperation(), vectorFour); + hState2.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + + EXPECT_TRUE(hState1 == hState2); +} + +TEST_F(HybridStateTest, hybridStatesNotEqualQuantumStates) { + auto hState1 = HybridState(vectorZeroTwoFour, 3); + auto hState2 = HybridState(vectorZeroTwoFour, 3); + const auto i0 = programBuilder.getI64IntegerAttr(0); + const auto i1 = programBuilder.getF64FloatAttr(-2.5); + const auto i2 = programBuilder.getF64FloatAttr(0.0); + + const mlir::Value val0 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i0); + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + + hState1.addIntegerValue(val0, 9); + hState1.addDoubleValue(val1, -2.5); + hState1.addDoubleValue(val2, 0.0); + hState1.propagateGate(hOp.getOperation(), vectorFour); + hState1.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + + hState2.addIntegerValue(val0, 9); + hState2.addDoubleValue(val1, -2.5); + hState2.addDoubleValue(val2, 0.0); + hState2.propagateGate(hOp.getOperation(), vectorFour); + hState2.propagateGate(xOp.getOperation(), vectorTwo); + + EXPECT_TRUE(hState1 != hState2); +} + +TEST_F(HybridStateTest, hybridStatesNotEqualInteger) { + auto hState1 = HybridState(vectorZeroTwoFour, 3); + auto hState2 = HybridState(vectorZeroTwoFour, 3); + const auto i0 = programBuilder.getI64IntegerAttr(0); + const auto i1 = programBuilder.getF64FloatAttr(-2.5); + const auto i2 = programBuilder.getF64FloatAttr(0.0); + + const mlir::Value val0 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i0); + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + + hState1.addIntegerValue(val0, 9); + hState1.addDoubleValue(val1, -2.5); + hState1.addDoubleValue(val2, 0.0); + hState1.propagateGate(hOp.getOperation(), vectorFour); + hState1.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + + hState2.addIntegerValue(val0, 10); + hState2.addDoubleValue(val1, -2.5); + hState2.addDoubleValue(val2, 0.0); + hState2.propagateGate(hOp.getOperation(), vectorFour); + hState2.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + + EXPECT_TRUE(hState1 != hState2); +} + +TEST_F(HybridStateTest, hybridStatesNotEqualDouble) { + auto hState1 = HybridState(vectorZeroTwoFour, 3); + auto hState2 = HybridState(vectorZeroTwoFour, 3); + const auto i0 = programBuilder.getI64IntegerAttr(0); + const auto i1 = programBuilder.getF64FloatAttr(-2.5); + const auto i2 = programBuilder.getF64FloatAttr(0.0); + + const mlir::Value val0 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i0); + const mlir::Value val1 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i1); + const mlir::Value val2 = mlir::arith::ConstantOp::create( + programBuilder, programBuilder.getLoc(), i2); + + hState1.addIntegerValue(val0, 9); + hState1.addDoubleValue(val1, -2.5); + hState1.addDoubleValue(val2, 0.0); + hState1.propagateGate(hOp.getOperation(), vectorFour); + hState1.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + + hState2.addIntegerValue(val0, 9); + hState2.addDoubleValue(val1, -2.5); + hState2.addDoubleValue(val2, 0.5); + hState2.propagateGate(hOp.getOperation(), vectorFour); + hState2.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + + EXPECT_TRUE(hState1 != hState2); +} + +} // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_quantumState.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_quantumState.cpp new file mode 100644 index 0000000000..1f7b63861a --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_quantumState.cpp @@ -0,0 +1,418 @@ +/* + * 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/Optimizations/ConstantPropagation/QuantumState.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +using namespace mlir::qco; + +class QuantumStateTest : public testing::Test { +protected: + mlir::MLIRContext context; + QCOProgramBuilder programBuilder; + QCOProgramBuilder referenceBuilder; + std::vector fourQubits = {0, 1, 2, 3}; + std::vector vectorZero = {0}; + std::vector vectorOne = {1}; + std::vector vectorTwo = {2}; + std::vector vectorThree = {3}; + std::vector vectorFour = {4}; + std::vector vectorZeroOne = {0, 1}; + std::vector vectorOneThree = {1, 3}; + std::vector vectorTwoOne = {2, 1}; + std::vector vectorZeroTwoFour = {0, 2, 4}; + + IdOp idOp; + HOp hOp; + XOp xOp; + ZOp zOp; + SOp sOp; + SXOp sxOp; + SXdgOp sxdgOp; + TdgOp tdgOp; + UOp uOp; + DCXOp dcxOp; + SWAPOp swapOp; + iSWAPOp iSwapOp; + RZOp rzOp; + POp pOp; + ECROp ecrOp; + RXXOp rxxOp; + RYYOp ryyOp; + RZXOp rzxOp; + RZZOp rzzOp; + XXPlusYYOp xxPlusyyOp; + XXMinusYYOp xxMinusyyOp; + + QuantumStateTest() : programBuilder(&context), referenceBuilder(&context) {} + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.initialize(); + referenceBuilder.initialize(); + + auto q = programBuilder.allocQubitRegister(4); + idOp = IdOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + hOp = HOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + xOp = XOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + zOp = ZOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + sOp = SOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + sxOp = SXOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + sxdgOp = SXdgOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[0]); + uOp = UOp::create(programBuilder, programBuilder.getLoc(), {q[0].getType()}, + {q[0], q[1], q[2], q[3]}); + tdgOp = TdgOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[0]); + dcxOp = DCXOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1]); + swapOp = SWAPOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1]); + iSwapOp = iSWAPOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1]); + rzOp = RZOp::create(programBuilder, programBuilder.getLoc(), + {q[0].getType()}, {q[0], q[1]}); + pOp = POp::create(programBuilder, programBuilder.getLoc(), {q[0].getType()}, + {q[0], q[1]}); + ecrOp = ECROp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1]); + rxxOp = RXXOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1], q[2]); + ryyOp = RYYOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1], q[2]); + rzxOp = RZXOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1], q[2]); + rzzOp = RZZOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1], q[2]); + xxPlusyyOp = XXPlusYYOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], q[1], + q[2], q[3]); + xxMinusyyOp = XXMinusYYOp::create(programBuilder, programBuilder.getLoc(), + q[0].getType(), q[1].getType(), q[0], + q[1], q[2], q[3]); + ; + } +}; + +TEST_F(QuantumStateTest, applyHGate) { + auto qState = QuantumState(vectorZero, 4); + qState.propagateGate(hOp.getOperation(), vectorZero); + + EXPECT_THAT(qState.toString(), + testing::HasSubstr("|0> -> 0.71, |1> -> 0.71")); +} + +TEST_F(QuantumStateTest, applyHGateToThirdQubit) { + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorTwo); + + EXPECT_THAT(qState.toString(), + testing::HasSubstr("|0000> -> 0.71, |0100> -> 0.71")); +} + +TEST_F(QuantumStateTest, applyHHGateToThirdQubit) { + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(hOp.getOperation(), vectorTwo); + + EXPECT_THAT(qState.toString(), testing::HasSubstr("|0000> -> 1")); +} + +TEST_F(QuantumStateTest, applyHZGateToThirdQubit) { + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(zOp.getOperation(), vectorTwo); + + EXPECT_THAT(qState.toString(), + testing::HasSubstr("|0000> -> 0.71, |0100> -> -0.71")); +} + +TEST_F(QuantumStateTest, applyHZHGateToThirdQubit) { + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(zOp.getOperation(), vectorTwo); + qState.propagateGate(hOp.getOperation(), vectorTwo); + + EXPECT_THAT(qState.toString(), testing::HasSubstr("|0100> -> 1")); +} + +TEST_F(QuantumStateTest, applyHGatesToTwoQubits) { + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(xOp.getOperation(), vectorZero); + + EXPECT_THAT(qState.toString(), + testing::HasSubstr("|0001> -> 0.71, |0101> -> 0.71")); +} + +TEST_F(QuantumStateTest, applyParametrizedGateToThirdQubit) { + std::vector params = {1, 0.5, 2}; + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(uOp.getOperation(), vectorTwo, {}, params); + + EXPECT_THAT( + qState.toString(), + testing::HasSubstr("|0000> -> 0.76 - i0.31, |0100> -> -0.20 + i0.53")); +} + +TEST_F(QuantumStateTest, applyTwoQubitGate) { + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorOne); + qState.propagateGate(sOp.getOperation(), vectorOne); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(tdgOp.getOperation(), vectorTwo); + qState.propagateGate(dcxOp.getOperation(), vectorTwoOne); + + EXPECT_THAT( + qState.toString(), + testing::HasSubstr("|0000> -> 0.50, |0010> -> 0.35 - i0.35, " + "|0100> -> 0.35 + i0.35, |0110> -> 0.00 + i0.50")); +} + +TEST_F(QuantumStateTest, applyTwoQubitGateReversedOrd) { + std::vector vectorOneTwo = {1, 2}; + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorOne); + qState.propagateGate(sOp.getOperation(), vectorOne); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(tdgOp.getOperation(), vectorTwo); + qState.propagateGate(dcxOp.getOperation(), vectorOneTwo); + + EXPECT_THAT( + qState.toString(), + testing::HasSubstr("|0000> -> 0.50, |0010> -> 0.35 + i0.35, |0100> -> " + "0.00 + i0.50, |0110> -> 0.35 - i0.35")); +} + +TEST_F(QuantumStateTest, applySwapGate) { + std::vector vectorOneThree = {1, 3}; + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorOne); + qState.propagateGate(swapOp.getOperation(), vectorOneThree); + + EXPECT_THAT(qState.toString(), + testing::HasSubstr("|0000> -> 0.71, |1000> -> 0.71")); +} + +TEST_F(QuantumStateTest, applyControlledGate1) { + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorOne); + qState.propagateGate(xOp.getOperation(), vectorThree); + qState.propagateGate(xOp.getOperation(), vectorThree, vectorOne); + + EXPECT_THAT(qState.toString(), + testing::HasSubstr("|0010> -> 0.71, |1000> -> 0.71")); +} + +TEST_F(QuantumStateTest, applyControlledGate2) { + auto qState = QuantumState(fourQubits, 8); + qState.propagateGate(hOp.getOperation(), vectorZero); + qState.propagateGate(hOp.getOperation(), vectorOne); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(xOp.getOperation(), vectorThree, vectorZeroOne); + + EXPECT_THAT( + qState.toString(), + testing::HasSubstr( + "|0000> -> 0.35, |0001> -> 0.35, |0010> -> 0.35, |0100> -> 0.35, " + "|0101> -> 0.35, |0110> -> 0.35, |1011> -> 0.35, |1111> -> 0.35")); +} + +TEST_F(QuantumStateTest, applyControlledTwoQubitGate) { + auto qState = QuantumState(fourQubits, 4); + qState.propagateGate(hOp.getOperation(), vectorThree); + qState.propagateGate(hOp.getOperation(), vectorTwo); + qState.propagateGate(swapOp.getOperation(), vectorTwoOne, vectorThree); + + EXPECT_THAT( + qState.toString(), + testing::HasSubstr( + "|0000> -> 0.50, |0100> -> 0.50, |1000> -> 0.50, |1010> -> 0.50")); +} + +TEST_F(QuantumStateTest, propagateGateCheckErrorIfTooManyAmplitudesAreNonzero) { + auto qState = QuantumState(fourQubits, 2); + qState.propagateGate(hOp.getOperation(), vectorThree); + qState.propagateGate(xOp.getOperation(), vectorTwo, vectorThree); + + EXPECT_FALSE(qState.isTop()); + qState.propagateGate(hOp.getOperation(), vectorTwo); + EXPECT_TRUE(qState.isTop()); +} + +TEST_F(QuantumStateTest, doMeasurementWithZeroResult) { + auto qState = QuantumState(vectorZero, 2); + const auto [states, availableStates] = qState.measureQubit(0); + + EXPECT_TRUE(availableStates.size() == 1); + auto [probability, qs] = states.at(0); + EXPECT_TRUE(qState == *qs.get()); + EXPECT_DOUBLE_EQ(probability, 1); +} + +TEST_F(QuantumStateTest, doMeasurementWithOneResult) { + auto qState = QuantumState(vectorZero, 2); + qState.propagateGate(xOp.getOperation(), vectorZero); + const auto [states, availableStates] = qState.measureQubit(0); + + EXPECT_TRUE(availableStates.size() == 1); + auto [probability, qs] = states.at(1); + EXPECT_TRUE(qState == *qs.get()); + EXPECT_DOUBLE_EQ(probability, 1); +} + +TEST_F(QuantumStateTest, doMeasurementWithTwoResults) { + auto qState = QuantumState(vectorZeroOne, 2); + qState.propagateGate(hOp.getOperation(), vectorZero); + qState.propagateGate(xOp.getOperation(), vectorOne, vectorZero); + const auto [states, availableStates] = qState.measureQubit(0); + + auto const zeroReference = QuantumState(vectorZeroOne, 2); + auto oneReference = QuantumState(vectorZeroOne, 2); + oneReference.propagateGate(xOp.getOperation(), vectorZero); + oneReference.propagateGate(xOp.getOperation(), vectorOne); + + EXPECT_TRUE(availableStates.size() == 2); + auto [probabilityZero, qsZero] = states.at(0); + EXPECT_TRUE(zeroReference == *qsZero.get()); + EXPECT_DOUBLE_EQ(probabilityZero, 0.5); + auto [probabilityOne, qsOne] = states.at(1); + EXPECT_TRUE(oneReference == *qsOne.get()); + EXPECT_DOUBLE_EQ(probabilityOne, 0.5); +} + +TEST_F(QuantumStateTest, doResetWithOnlyZeros) { + auto qState = QuantumState(vectorZero, 2); + const auto [states, availableStates] = qState.resetQubit(0); + + EXPECT_TRUE(availableStates.size() == 1); + auto [probability, qs] = states.at(0); + EXPECT_TRUE(qState == *qs.get()); + EXPECT_DOUBLE_EQ(probability, 1); +} + +TEST_F(QuantumStateTest, doResetWithOnlyOnes) { + auto qState = QuantumState(vectorZero, 2); + qState.propagateGate(xOp.getOperation(), vectorZero); + const auto [states, availableStates] = qState.resetQubit(0); + + auto const refState = QuantumState(vectorZero, 2); + + EXPECT_TRUE(availableStates.size() == 1); + auto [probability, qs] = states.at(1); + EXPECT_TRUE(refState == *qs.get()); + EXPECT_DOUBLE_EQ(probability, 1); +} + +TEST_F(QuantumStateTest, doResetWithZerosAndOnes) { + auto qState = QuantumState(vectorZeroOne, 2); + qState.propagateGate(hOp.getOperation(), vectorZero); + qState.propagateGate(xOp.getOperation(), vectorOne, vectorZero); + const auto [states, availableStates] = qState.resetQubit(0); + + auto const zeroReference = QuantumState(vectorZeroOne, 2); + auto oneReference = QuantumState(vectorZeroOne, 2); + oneReference.propagateGate(xOp.getOperation(), vectorOne); + + EXPECT_TRUE(availableStates.size() == 2); + auto [probabilityZero, qsZero] = states.at(0); + EXPECT_DOUBLE_EQ(probabilityZero, 0.5); + auto [probabilityOne, qsOne] = states.at(1); + EXPECT_DOUBLE_EQ(probabilityOne, 0.5); + EXPECT_TRUE(*qsZero.get() == zeroReference); + EXPECT_TRUE(*qsOne.get() == oneReference); +} + +TEST_F(QuantumStateTest, unifyTwoQuantumStates) { + auto qState1 = QuantumState(vectorZeroTwoFour, 10); + qState1.propagateGate(hOp.getOperation(), vectorFour); + qState1.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + qState1.propagateGate(xOp.getOperation(), vectorZero, vectorTwo); + + auto qState2 = QuantumState(vectorOneThree, 10); + qState2.propagateGate(hOp.getOperation(), vectorThree); + qState2.propagateGate(xOp.getOperation(), vectorOne, vectorThree); + + const QuantumState unified = qState1.unify(qState2); + + EXPECT_THAT(unified.toString(), + testing::HasSubstr("|00000> -> 0.50, |01010> -> 0.50, " + "|10101> -> 0.50, |11111> -> 0.50")); +} + +TEST_F(QuantumStateTest, unifyTooLargeQuantumStates) { + auto qState1 = QuantumState(vectorZeroTwoFour, 3); + qState1.propagateGate(hOp.getOperation(), vectorFour); + qState1.propagateGate(xOp.getOperation(), vectorTwo, vectorFour); + qState1.propagateGate(xOp.getOperation(), vectorZero, vectorTwo); + + auto qState2 = QuantumState(vectorOneThree, 3); + qState2.propagateGate(hOp.getOperation(), vectorThree); + qState2.propagateGate(xOp.getOperation(), vectorOne, vectorThree); + EXPECT_FALSE(qState1.isTop()); + EXPECT_FALSE(qState2.isTop()); + const auto qs = qState1.unify(qState2); + EXPECT_TRUE(qs.isTop()); +} + +TEST_F(QuantumStateTest, applyVariousGates) { + std::vector paramsZero = {1.0}; + std::vector paramsOne = {2.7}; + std::vector paramsTwo = {1.3, 2.0}; + std::vector paramsThree = {-1.3, 2.0}; + auto qState = QuantumState(vectorZeroOne, 4); + qState.propagateGate(idOp.getOperation(), vectorZero); + qState.propagateGate(sxOp.getOperation(), vectorZero); + qState.propagateGate(sxdgOp.getOperation(), vectorOne); + qState.propagateGate(iSwapOp.getOperation(), vectorZeroOne); + qState.propagateGate(rzOp.getOperation(), vectorOne, {}, paramsZero); + qState.propagateGate(pOp.getOperation(), vectorZero, {}, paramsOne); + qState.propagateGate(ecrOp.getOperation(), vectorZeroOne); + qState.propagateGate(rxxOp.getOperation(), vectorZeroOne, {}, paramsZero); + qState.propagateGate(ryyOp.getOperation(), vectorZeroOne, {}, paramsZero); + qState.propagateGate(rzxOp.getOperation(), vectorZeroOne, {}, paramsZero); + qState.propagateGate(rzzOp.getOperation(), vectorZeroOne, {}, paramsZero); + qState.propagateGate(xxMinusyyOp.getOperation(), vectorZeroOne, {}, + paramsTwo); + qState.propagateGate(xxPlusyyOp.getOperation(), vectorZeroOne, {}, + paramsThree); + + EXPECT_THAT( + qState.toString(), + testing::HasSubstr("|00> -> 0.58 - i0.07, |01> -> -0.14 + i0.04, |10> " + "-> 0.23 + i0.15, |11> -> -0.10 - i0.74")); +} + +} // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_unionTable.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_unionTable.cpp new file mode 100644 index 0000000000..23e09cc040 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_unionTable.cpp @@ -0,0 +1,1322 @@ +/* + * 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/Optimizations/ConstantPropagation/UnionTable.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +using namespace mlir::qco; + +class UnionTableTest : public testing::Test { +protected: + mlir::MLIRContext context; + QCOProgramBuilder programBuilder; + UnionTable ut = UnionTable(4, 4); + + HOp hOp; + XOp xOp; + SWAPOp swapOp; + GPhaseOp gPhaseOp; + + mlir::Value v0; + mlir::Value v1; + mlir::Value v2; + mlir::Value v3; + mlir::Value v4; + mlir::Value v5; + mlir::Value v6; + mlir::Value v7; + mlir::Value v8; + mlir::Value v9; + mlir::Value v10; + mlir::Value i0; + mlir::Value i1; + mlir::Value i2; + + std::vector q0; + std::vector q1; + std::vector q2; + std::vector q3; + std::vector q4; + std::vector q5; + std::vector q6; + std::vector q7; + std::vector q8; + std::vector q9; + std::vector q10; + + UnionTableTest() : programBuilder(&context) {} + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.initialize(); + + auto q = programBuilder.allocQubitRegister(11); + hOp = HOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + xOp = XOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + gPhaseOp = GPhaseOp::create(programBuilder, programBuilder.getLoc(), 1.0); + swapOp = SWAPOp::create(programBuilder, programBuilder.getLoc(), + {q[0].getType(), q[1].getType()}, {q[0], q[1]}); + + v0 = q[0]; + v1 = q[1]; + v2 = q[2]; + v3 = q[3]; + v4 = q[4]; + v5 = q[5]; + v6 = q[6]; + v7 = q[7]; + v8 = q[8]; + v9 = q[9]; + v10 = q[10]; + + q0 = {v0}; + q1 = {v1}; + q2 = {v2}; + q3 = {v3}; + q4 = {v4}; + q5 = {v5}; + q6 = {v6}; + q7 = {v7}; + q8 = {v8}; + q9 = {v9}; + q10 = {v10}; + + const auto iAttr = programBuilder.getI64IntegerAttr(0); + + i0 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + i1 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + i2 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + ut.propagateQubitAlloc(v2); + ut.propagateQubitAlloc(v3); + } +}; + +TEST_F(UnionTableTest, ApplyHGate) { + ut.propagateGate(hOp, q0, q5); + + EXPECT_THAT( + ut.toString(), + testing::HasSubstr( + "Qubits: 0, HybridStates: {{|0> -> 0.71, |1> -> 0.71}: p = 1.00;}")); +} + +TEST_F(UnionTableTest, ApplyHGateToThirdQubit) { + ut.propagateGate(hOp, q2, q5); + + EXPECT_THAT( + ut.toString(), + testing::HasSubstr( + "Qubits: 2, HybridStates: {{|0> -> 0.71, |1> -> 0.71}: p = 1.00;}")); +} + +TEST_F(UnionTableTest, ApplyQuantumControlledGate) { + ut.propagateGate(hOp, q1, q5); + ut.propagateGate(xOp, q3, q6); + ut.propagateGate(xOp, q6, q7, q5, q8); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 0, HybridStates: {{|0> -> 1.00}: p = 1.00;}")); + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 2, HybridStates: {{|0> -> 1.00}: p = 1.00;}")); + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 31, HybridStates: {{|01> -> 0.71, " + "|10> -> 0.71}: p = 1.00;}")); +} + +TEST_F(UnionTableTest, ApplyClassicalControlledGateThatsFalse) { + std::vector classicalControl = {i1}; + ut.propagateIntAlloc(i0, 1); + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q1, q5); + ut.propagateGate(xOp, q3, q6); + ut.propagateGate(xOp, q6, q7, q5, q8, classicalControl); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 31, HybridStates: {{|10> " + "-> 0.71, |11> -> 0.71}: integerValue0 = 0; p = 1.00;}")); +} + +TEST_F(UnionTableTest, ApplyClassicalControlledGateThatsTrue) { + std::vector classicalControl = {i0}; + ut.propagateIntAlloc(i0, 1); + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q1, q5); + ut.propagateGate(xOp, q3, q6); + ut.propagateGate(xOp, q6, q7, q5, q8, classicalControl); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 31, HybridStates: {{|01> " + "-> 0.71, |10> -> 0.71}: integerValue0 = 1; p = 1.00;}")); +} + +TEST_F(UnionTableTest, ApplyClassicalControlledGPhase) { + std::vector classicalControl0 = {i0}; + std::vector classicalControl1 = {i1}; + ut.propagateIntAlloc(i0, 1); + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q1, q5); + + const auto stateBeforeGPhase = ut.toString(); + + ut.propagateGate(gPhaseOp, {}, {}, {}, {}, classicalControl0); + ut.propagateGate(gPhaseOp, {}, {}, {}, {}, {}, classicalControl1); + + EXPECT_EQ(stateBeforeGPhase, ut.toString()); +} + +TEST_F(UnionTableTest, ApplyNegClassicalControlledGateThatsTrue) { + std::vector classicalControl = {i0}; + ut.propagateIntAlloc(i0, 1); + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q1, q5); + ut.propagateGate(xOp, q3, q6); + ut.propagateGate(xOp, q6, q7, q5, q8, {}, classicalControl); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 31, HybridStates: {{|10> " + "-> 0.71, |11> -> 0.71}: integerValue0 = 1; p = 1.00;}")); +} + +TEST_F(UnionTableTest, ApplyNegClassicalControlledGateThatsFalse) { + std::vector classicalControl = {i1}; + ut.propagateIntAlloc(i0, 1); + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q1, q5); + ut.propagateGate(xOp, q3, q6); + ut.propagateGate(xOp, q6, q7, q5, q8, {}, classicalControl); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 31, HybridStates: {{|01> " + "-> 0.71, |10> -> 0.71}: integerValue0 = 0; p = 1.00;}")); +} + +TEST_F(UnionTableTest, ApplyPosNegClassicalControlledGateThatsFalse) { + std::vector classicalControlZero = {i0}; + std::vector classicalControlOne = {i1}; + ut.propagateIntAlloc(i0, 0); + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q1, q5); + ut.propagateGate(xOp, q3, q6); + ut.propagateGate(xOp, q6, q7, q5, q8, classicalControlOne, + classicalControlZero); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 31, HybridStates: {{|10> " + "-> 0.71, |11> -> 0.71}: integerValue0 = 0, " + "integerValue1 = 0; p = 1.00;}")); +} + +TEST_F(UnionTableTest, ApplyPosNegClassicalControlledGateThatsTrue) { + std::vector classicalControlTrue = {i0}; + std::vector classicalControlFalse = {i1}; + ut.propagateIntAlloc(i0, 1); + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q1, q5); + ut.propagateGate(xOp, q3, q6); + ut.propagateGate(xOp, q6, q7, q5, q8, classicalControlTrue, + classicalControlFalse); + + EXPECT_THAT( + ut.toString(), + testing::AnyOf( + testing::HasSubstr("Qubits: 31, HybridStates: {{|01> " + "-> 0.71, |10> -> 0.71}: integerValue0 = 1, " + "integerValue1 = 0; p = 1.00;}"), + testing::HasSubstr("Qubits: 31, HybridStates: {{|01> " + "-> 0.71, |10> -> 0.71}: integerValue0 = 0, " + "integerValue1 = 1; p = 1.00;}"))); +} + +TEST_F(UnionTableTest, ApplyControlledTwoBitGate) { + std::vector classicalControlTrue = {i0, i2}; + std::vector classicalControlFalse = {i1}; + ut.propagateIntAlloc(i0, 1); + ut.propagateIntAlloc(i1, 0); + ut.propagateIntAlloc(i2, 1); + ut.propagateGate(hOp, q1, q4); + ut.propagateGate(xOp, q3, q5); + ut.propagateGate(xOp, q5, q6, q4, q7, classicalControlTrue, + classicalControlFalse); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 31, HybridStates: {{|01> " + "-> 0.71, |10> -> 0.71}: ")); +} + +TEST_F(UnionTableTest, doMeasurementWithOneResult) { + ut.propagateGate(xOp, q0, q4); + ut.propagateIntAlloc(i0, 10); + ut.propagateMeasurement(v4, v5, i0); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 0, HybridStates: {{|1> " + "-> 1.00}: integerValue0 = 1; p = 1.00;}")); +} + +TEST_F(UnionTableTest, doMeasurementWithTwoResults) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateIntAlloc(i0, 10); + ut.propagateMeasurement(v5, v7, i0); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 10, HybridStates: {{|00> " + "-> 1.00}: integerValue0 = 0; p = 0.50; {|11> " + "-> 1.00}: integerValue0 = 1; p = 0.50;}")); +} + +TEST_F(UnionTableTest, doMeasurementWithNegPosCtrl) { + std::vector ctrl = {i0}; + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateIntAlloc(i0, 0); + ut.propagateMeasurement(v5, v7, i0, ctrl); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 10, HybridStates: {{|00> " + "-> 0.71, |11> -> 0.71}: integerValue0 = 0; p = 1.00;}")); +} + +TEST_F(UnionTableTest, doMeasurementWithPosNegCtrl) { + std::vector ctrl = {i0}; + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateIntAlloc(i0, 10); + ut.propagateMeasurement(v5, v7, i0, {}, ctrl); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 10, HybridStates: {{|00> " + "-> 0.71, |11> -> 0.71}: integerValue0 = 10; p = 1.00;}")); +} + +TEST_F(UnionTableTest, doResetWithOneResult) { + ut.propagateGate(xOp, q0, q4); + ut.propagateReset(v4, v5); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 0, HybridStates: {{|0> " + "-> 1.00}: p = 1.00;}")); +} + +TEST_F(UnionTableTest, doResetWithTwoResults) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateReset(v6, v7); + + EXPECT_THAT( + ut.toString(), + testing::HasSubstr("Qubits: 10, HybridStates: {{|00> " + "-> 1.00}: p = 0.50; {|10> -> 1.00}: p = 0.50;}")); +} + +TEST_F(UnionTableTest, swapGateApplicationDifferentStates) { + std::vector swapTargets = {v7, v2}; + std::vector swapDestinations = {v8, v9}; + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateGate(xOp, q5, q7); + ut.propagateGate(swapOp, swapTargets, swapDestinations); + ut.propagateGate(xOp, q8, q10); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 20, HybridStates: {{|01> -> 0.71, " + "|10> -> 0.71}: p = 1.00;}")); + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 1, HybridStates: {{|1> -> 1.00}: p = 1.00;}")); +} + +TEST_F(UnionTableTest, swapGateApplicationSameState) { + std::vector swapTargets = {v6, v7}; + std::vector swapDestinations = {v8, v9}; + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(hOp, q1, q5, q4, q6); + ut.propagateGate(xOp, q5, q7); + ut.propagateGate(swapOp, swapTargets, swapDestinations); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 10, HybridStates: {{|01> -> 0.71, " + "|10> -> 0.50, |11> -> 0.50}: p = 1.00;}")); +} + +class UnionTableWithoutSetupAllocationsTest : public testing::Test { +protected: + mlir::MLIRContext context; + QCOProgramBuilder programBuilder; + QCOProgramBuilder referenceBuilder; + + HOp hOp; + XOp xOp; + + mlir::Value v0; + mlir::Value v1; + mlir::Value v2; + mlir::Value v3; + mlir::Value v4; + mlir::Value v5; + mlir::Value v6; + mlir::Value i0; + mlir::Value i1; + + std::vector q0; + std::vector q1; + std::vector q2; + std::vector q3; + std::vector q4; + std::vector q5; + std::vector q6; + + UnionTableWithoutSetupAllocationsTest() + : programBuilder(&context), referenceBuilder(&context) {} + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.initialize(); + referenceBuilder.initialize(); + + auto q = programBuilder.allocQubitRegister(9); + hOp = HOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + xOp = XOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + v0 = q[0]; + v1 = q[1]; + v2 = q[2]; + v3 = q[3]; + v4 = q[4]; + v5 = q[5]; + v6 = q[6]; + + q0 = {v0}; + q1 = {v1}; + q2 = {v2}; + q3 = {v3}; + q4 = {v4}; + q5 = {v5}; + q6 = {v6}; + + const auto iAttr = programBuilder.getI64IntegerAttr(0); + + i0 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + i1 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + } +}; + +TEST_F(UnionTableWithoutSetupAllocationsTest, propagateQubitAlloc) { + auto ut = UnionTable(4, 2); + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 0, HybridStates: {{|0> -> 1.00}: p = 1.00;}")); + EXPECT_THAT(ut.toString(), + testing::HasSubstr( + "Qubits: 1, HybridStates: {{|0> -> 1.00}: p = 1.00;}")); +} + +TEST_F(UnionTableWithoutSetupAllocationsTest, applyGateAndGetToTop) { + auto ut = UnionTable(2, 1); + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + ut.propagateGate(hOp, q0, q2); + ut.propagateGate(hOp, q1, q3); + ut.propagateGate(xOp, q2, q4, q3, q5); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 10, HybridStates: {TOP}")); +} + +TEST_F(UnionTableWithoutSetupAllocationsTest, doMeasurementsAndGetToTop) { + auto ut = UnionTable(4, 1); + ut.propagateQubitAlloc(v1); + ut.propagateQubitAlloc(v2); + ut.propagateIntAlloc(i0, 10); + ut.propagateGate(hOp, q1, q3); + ut.propagateMeasurement(v3, v4, i0); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 0, HybridStates: {TOP}")); +} + +TEST_F(UnionTableWithoutSetupAllocationsTest, doMeasurementsOnTop) { + auto ut = UnionTable(2, 2); + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + ut.propagateIntAlloc(i0, 10); + ut.propagateGate(hOp, q0, q2); + ut.propagateGate(xOp, q1, q3, q2, q4); + ut.propagateGate(hOp, q3, q5); // State enters TOP + ut.propagateMeasurement(v5, v6, i0); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 10, HybridStates: {TOP}")); +} + +TEST_F(UnionTableWithoutSetupAllocationsTest, doResetOnTop) { + auto ut = UnionTable(2, 2); + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + ut.propagateGate(hOp, q0, q2); + ut.propagateGate(xOp, q1, q3, q2, q4); + ut.propagateGate(hOp, q3, q5); // State enters TOP + ut.propagateReset(v5, v6); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 10, HybridStates: {TOP}")); +} + +TEST_F(UnionTableWithoutSetupAllocationsTest, unifyTooLargeHybridStates) { + auto ut = UnionTable(4, 1); + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + ut.propagateQubitAlloc(v2); + ut.propagateIntAlloc(i0, 10); + ut.propagateGate(hOp, q0, q3); + ut.propagateMeasurement(v3, v4, i0); + ut.propagateGate(xOp, q1, q5, q4, q6); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 10, HybridStates: {TOP}")); +} + +TEST_F(UnionTableWithoutSetupAllocationsTest, globalPhaseClassicallyDependent) { + const auto sOp = + SOp::create(programBuilder, programBuilder.getLoc(), v0.getType(), v0); + std::vector classicalIndexVecZero = {i0}; + std::vector classicalIndexVecOne = {i1}; + auto ut = UnionTable(4, 4); + ut.propagateQubitAlloc(v0); + ut.propagateGate(xOp, q0, q1); + ut.propagateDoubleAlloc(i0, 0.0); + ut.propagateDoubleAlloc(i1, 1.0); + + const auto globalPhaseCondsFalse = ut.globalPhaseThatIsAdded( + sOp, v1, {}, classicalIndexVecZero, classicalIndexVecOne); + const auto globalPhaseCondsTrue = ut.globalPhaseThatIsAdded( + sOp, v1, {}, classicalIndexVecOne, classicalIndexVecZero); + + ASSERT_FALSE(globalPhaseCondsFalse.has_value()); + ASSERT_TRUE(globalPhaseCondsTrue.has_value()); + ASSERT_EQ(std::numbers::pi / 2, globalPhaseCondsTrue); +} + +TEST_F(UnionTableWithoutSetupAllocationsTest, globalPhaseVariousGates) { + const auto idOp = + IdOp::create(programBuilder, programBuilder.getLoc(), v0.getType(), v0); + const auto sdgOp = + SdgOp::create(programBuilder, programBuilder.getLoc(), v0.getType(), v0); + const auto tOp = + TOp::create(programBuilder, programBuilder.getLoc(), v0.getType(), v0); + const auto tdgOp = + TdgOp::create(programBuilder, programBuilder.getLoc(), v0.getType(), v0); + auto ut = UnionTable(2, 1); + ut.propagateQubitAlloc(v0); + ut.propagateGate(xOp, q0, q1); + + const auto globalPhaseId = ut.globalPhaseThatIsAdded(idOp, v1); + const auto globalPhaseSdg = ut.globalPhaseThatIsAdded(sdgOp, v1); + const auto globalPhaseT = ut.globalPhaseThatIsAdded(tOp, v1); + const auto globalPhaseTdg = ut.globalPhaseThatIsAdded(tdgOp, v1); + + ASSERT_TRUE(globalPhaseId.has_value()); + ASSERT_TRUE(globalPhaseSdg.has_value()); + ASSERT_TRUE(globalPhaseT.has_value()); + ASSERT_TRUE(globalPhaseTdg.has_value()); + ASSERT_EQ(0.0, globalPhaseId); + ASSERT_EQ(3.0 * std::numbers::pi / 2, globalPhaseSdg); + ASSERT_EQ(std::numbers::pi / 4, globalPhaseT); + ASSERT_EQ(-std::numbers::pi / 4, globalPhaseTdg); +} + +class UnionTablePropertiesTest : public testing::Test { +protected: + mlir::MLIRContext context; + QCOProgramBuilder programBuilder; + UnionTable ut = UnionTable(3, 2); + + HOp hOp; + XOp xOp; + ZOp zOp; + SOp sOp; + SWAPOp swapOp; + + mlir::Value v0; + mlir::Value v1; + mlir::Value v2; + mlir::Value v3; + mlir::Value v4; + mlir::Value v5; + mlir::Value v6; + mlir::Value v7; + mlir::Value v8; + mlir::Value v9; + mlir::Value v10; + mlir::Value v11; + mlir::Value v12; + mlir::Value v13; + mlir::Value v14; + mlir::Value i0; + mlir::Value i1; + + std::vector q0; + std::vector q1; + std::vector q2; + std::vector q3; + std::vector q4; + std::vector q5; + std::vector q6; + std::vector q7; + std::vector q8; + std::vector q9; + std::vector q10; + std::vector q11; + std::vector q12; + std::vector q13; + std::vector q14; + + UnionTablePropertiesTest() : programBuilder(&context) {} + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.initialize(); + + auto q = programBuilder.allocQubitRegister(15); + hOp = HOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + xOp = XOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + zOp = ZOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + sOp = SOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + swapOp = SWAPOp::create(programBuilder, programBuilder.getLoc(), + {q[0].getType(), q[1].getType()}, {q[0], q[1]}); + + v0 = q[0]; + v1 = q[1]; + v2 = q[2]; + v3 = q[3]; + v4 = q[4]; + v5 = q[5]; + v6 = q[6]; + v7 = q[7]; + v8 = q[8]; + v9 = q[9]; + v10 = q[10]; + v11 = q[11]; + v12 = q[12]; + v13 = q[13]; + v14 = q[14]; + + q0 = {v0}; + q1 = {v1}; + q2 = {v2}; + q3 = {v3}; + q4 = {v4}; + q5 = {v5}; + q6 = {v6}; + q7 = {v7}; + q8 = {v8}; + q9 = {v9}; + q10 = {v10}; + q11 = {v11}; + q12 = {v12}; + q13 = {v13}; + q14 = {v14}; + + const auto iAttr = programBuilder.getI64IntegerAttr(0); + + i0 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + i1 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + ut.propagateQubitAlloc(v2); + ut.propagateIntAlloc(i0, 0); + } +}; + +TEST_F(UnionTablePropertiesTest, alwaysZeroOneAreFalse) { + std::vector ctrl = {i0}; + ut.propagateGate(hOp, q0, q3); + ut.propagateGate(xOp, q1, q4, q3, q5); + ut.propagateGate(xOp, q2, q6, q4, q7); + ut.propagateGate(xOp, q5, q8); + ut.propagateMeasurement(v7, v9, i0); + ut.propagateGate(hOp, q8, q10, {}, {}, ctrl); + + EXPECT_FALSE(ut.isQubitAlwaysZero(v6)); + EXPECT_FALSE(ut.isQubitAlwaysOne(v10)); +} + +TEST_F(UnionTablePropertiesTest, alwaysZeroIsTrue) { + std::vector ctrl = {i0}; + ut.propagateGate(hOp, q0, q3); + ut.propagateGate(xOp, q1, q4, q3, q5); + ut.propagateGate(xOp, q2, q6, q4, q7); + ut.propagateGate(xOp, q5, q8); + ut.propagateMeasurement(v7, v9, i0); + ut.propagateGate(xOp, q6, q10, {}, {}, ctrl); + ut.propagateGate(hOp, q8, q11, {}, {}, ctrl); + + EXPECT_TRUE(ut.isQubitAlwaysZero(v10)); +} + +TEST_F(UnionTablePropertiesTest, alwaysOneIsTrue) { + std::vector ctrl = {i0}; + std::vector qCtrl = {v7, v9}; + std::vector qCtrlNew = {v12, v13}; + ut.propagateGate(hOp, q0, q3); + ut.propagateGate(xOp, q1, q4, q3, q5); + ut.propagateGate(xOp, q2, q6, q4, q7); + ut.propagateGate(xOp, q5, q8); + ut.propagateMeasurement(v6, v9, i0); + ut.propagateGate(hOp, q8, q10, {}, {}, ctrl); + ut.propagateGate(zOp, q10, q11, qCtrl, qCtrlNew); + ut.propagateGate(hOp, q11, q14, {}, {}, ctrl); + + EXPECT_TRUE(ut.isQubitAlwaysOne(v14)); +} + +TEST_F(UnionTablePropertiesTest, bitAlwaysZeroIsTrueOneIsFalse) { + std::vector ctrl = {i0}; + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateMeasurement(v6, v7, i0); + ut.propagateGate(xOp, q5, q8, {}, {}, ctrl); + ut.propagateMeasurement(v8, v9, i1); + + EXPECT_FALSE(ut.isClassicalValueAlwaysTrue(i0)); + EXPECT_TRUE(ut.isClassicalValueAlwaysFalse(i1)); +} + +TEST_F(UnionTablePropertiesTest, bitAlwaysZeroIsFalseOneIsTrue) { + std::vector ctrl = {i0}; + ut.propagateIntAlloc(i1, 0); + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateMeasurement(v6, v7, i0); + ut.propagateGate(xOp, q5, q8, {}, {}, {}, ctrl); + ut.propagateMeasurement(v8, v9, i1); + + EXPECT_TRUE(ut.isClassicalValueAlwaysTrue(i1)); + EXPECT_FALSE(ut.isClassicalValueAlwaysFalse(i0)); +} + +TEST_F(UnionTablePropertiesTest, testAllTopAmplitudes) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateGate(xOp, q2, q7, q5, q8); + ut.propagateMeasurement(v7, v9, i0); + ut.propagateGate(hOp, q6, q10); + EXPECT_FALSE(ut.areStatesAllTop()); + ut.propagateGate(hOp, q8, q11); + EXPECT_TRUE(ut.areStatesAllTop()); + ut.propagateMeasurement(v10, v12, i0); + EXPECT_TRUE(ut.areStatesAllTop()); +} + +TEST_F(UnionTablePropertiesTest, testAllTopHybridStates) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateGate(xOp, q2, q7, q5, q8); + ut.propagateMeasurement(v7, v9, i0); + ut.propagateGate(hOp, q6, q10); + EXPECT_FALSE(ut.areStatesAllTop()); + ut.propagateMeasurement(v10, v11, i0); + EXPECT_TRUE(ut.areStatesAllTop()); +} + +TEST_F(UnionTablePropertiesTest, testMinusOneGlobalPhase) { + std::vector qCtrl = {v3, v4}; + ut.propagateGate(xOp, q0, q3); + ut.propagateGate(xOp, q1, q4); + ut.propagateGate(xOp, q2, q5); + const auto globalPhase = ut.globalPhaseThatIsAdded(zOp, v5, qCtrl); + EXPECT_TRUE(globalPhase.has_value()); + EXPECT_EQ(std::numbers::pi, globalPhase.value()); +} + +TEST_F(UnionTablePropertiesTest, testOneGlobalPhase) { + std::vector qCtrl = {v4, v5}; + ut.propagateGate(xOp, q0, q4); + ut.propagateGate(hOp, q1, q5); + const auto emptyGlobalPhase = ut.globalPhaseThatIsAdded(zOp, v5, q4); + const auto globalPhase = ut.globalPhaseThatIsAdded(zOp, v2, qCtrl); + EXPECT_FALSE(emptyGlobalPhase.has_value()); + EXPECT_TRUE(globalPhase.has_value()); + EXPECT_EQ(0.0, globalPhase.value()); +} + +TEST_F(UnionTablePropertiesTest, FindEquivalentClassicalValueOnZeroQubit) { + const llvm::DenseMap result = + ut.getValueThatIsEquivalentToQubit(v0); + ASSERT_FALSE(result.empty()); + ASSERT_TRUE(result.at(i0)); +} + +TEST_F(UnionTablePropertiesTest, FindEquivalentClassicalValueOnOneQubit) { + ut.propagateGate(xOp, q0, q4); + ut.propagateIntAlloc(i1, 10); + const llvm::DenseMap result = + ut.getValueThatIsEquivalentToQubit(v4); + ASSERT_FALSE(result.empty()); + ASSERT_TRUE(result.at(i1)); +} + +TEST_F(UnionTablePropertiesTest, FindInverseClassicalDoubleValueOnOneQubit) { + ut.propagateGate(xOp, q0, q4); + ut.propagateDoubleAlloc(i1, 0.0); + const llvm::DenseMap result = + ut.getValueThatIsEquivalentToQubit(v4); + ASSERT_FALSE(result.empty()); + ASSERT_FALSE(result.at(i1)); +} + +TEST_F(UnionTablePropertiesTest, FindEquivalentClassicalDoubleValueOnOneQubit) { + ut.propagateGate(xOp, q0, q4); + ut.propagateDoubleAlloc(i1, 10.0); + const llvm::DenseMap result = + ut.getValueThatIsEquivalentToQubit(v4); + ASSERT_FALSE(result.empty()); + ASSERT_TRUE(result.at(i1)); +} + +TEST_F(UnionTablePropertiesTest, FindEquivalentClassicalValue) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateMeasurement(v5, v7, i0); + const llvm::DenseMap result = + ut.getValueThatIsEquivalentToQubit(v7); + ASSERT_FALSE(result.empty()); + ASSERT_TRUE(result.at(i0)); +} + +TEST_F(UnionTablePropertiesTest, FindEquivalentReversedClassicalValue) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateGate(xOp, q6, q7); + ut.propagateMeasurement(v5, v8, i0); + const llvm::DenseMap result = + ut.getValueThatIsEquivalentToQubit(v7); + ASSERT_FALSE(result.empty()); + ASSERT_FALSE(result.at(i0)); +} + +TEST_F(UnionTablePropertiesTest, FindNoEquivalentClassicalValue) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateGate(hOp, q6, q7); + ut.propagateMeasurement(v5, v8, i0); + const llvm::DenseMap result = + ut.getValueThatIsEquivalentToQubit(v7); + ASSERT_TRUE(result.empty()); +} + +TEST_F(UnionTablePropertiesTest, hasAlwaysZeroProbabilityTest) { + std::vector classicalIndexVec = {i0}; + ut.propagateIntAlloc(i1, 20); + ut.propagateGate(hOp, q0, q3); + ut.propagateGate(xOp, q1, q4, q3, q5); + ut.propagateMeasurement(v4, v6, i0); + + llvm::DenseMap qubits0; + llvm::DenseMap classics0; + llvm::DenseMap qubits1; + llvm::DenseMap classics1; + qubits0[v5] = true; + qubits0[v6] = true; + qubits0[v2] = false; + classics0[i0] = true; + classics0[i1] = true; + qubits1[v5] = false; + qubits1[v6] = false; + qubits1[v2] = true; + classics1[i1] = false; + + ASSERT_FALSE(ut.hasAlwaysZeroProbability(qubits0, classics0)); + ASSERT_TRUE(ut.hasAlwaysZeroProbability(qubits1, classics1)); +} + +TEST_F(UnionTablePropertiesTest, ZeroIsAlwaysAntecedent) { + std::vector classicalIndexVec = {i0}; + ut.propagateMeasurement(v0, v4, i0); + ut.propagateGate(hOp, q1, q5); + ASSERT_TRUE(ut.isQubitImplied(v5, q4, classicalIndexVec, {})); +} + +TEST_F(UnionTablePropertiesTest, ImpliedQubit) { + std::vector classicalIndexVec = {i0}; + ut.propagateGate(hOp, q0, q4); + ut.propagateMeasurement(v4, v5, i0); + ut.propagateGate(hOp, q1, q6, q5, q7); + ASSERT_TRUE(ut.isQubitImplied(v7, q6, classicalIndexVec, {})); + ASSERT_FALSE(ut.isQubitImplied(v6, q7, classicalIndexVec, {})); +} + +TEST_F(UnionTablePropertiesTest, ImpliedQubitOnlyQubits) { + ut.propagateGate(hOp, q0, q4); + ut.propagateMeasurement(v4, v5, i0); + ut.propagateGate(hOp, q1, q6, q5, q7); + ASSERT_TRUE(ut.isQubitImplied(v7, q6, {}, {})); +} + +TEST_F(UnionTablePropertiesTest, ImpliedQubitOnlyClassicalValues) { + std::vector classicalIndexVec = {i0}; + ut.propagateGate(hOp, q0, q4); + ut.propagateMeasurement(v4, v5, i0); + ut.propagateGate(hOp, q5, q6, {}, {}, {}, classicalIndexVec); + ASSERT_TRUE(ut.isQubitImplied(v6, {}, classicalIndexVec, {})); +} + +TEST_F(UnionTablePropertiesTest, ImpliedQubitNegClassicalValues) { + std::vector classicalIndexVec = {i0}; + ut.propagateGate(hOp, q0, q4); + ut.propagateMeasurement(v4, v5, i0); + ut.propagateGate(hOp, q5, q6, {}, {}, classicalIndexVec); + ut.propagateGate(xOp, q6, q7); + ASSERT_TRUE(ut.isQubitImplied(v7, {}, {}, classicalIndexVec)); +} + +TEST_F(UnionTablePropertiesTest, globalPhaseOneQubit) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5); + + const auto globalPhase0 = ut.globalPhaseThatIsAdded(zOp, v4); + const auto globalPhase1 = ut.globalPhaseThatIsAdded(zOp, v5); + + ASSERT_FALSE(globalPhase0.has_value()); + ASSERT_TRUE(globalPhase1.has_value()); + ASSERT_EQ(std::numbers::pi, globalPhase1.value()); +} + +TEST_F(UnionTablePropertiesTest, noGlobalPhaseTwoQubitsA) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(hOp, q1, q5); + + const auto globalPhase0 = ut.globalPhaseThatIsAdded(zOp, v4, q5); + + ASSERT_FALSE(globalPhase0.has_value()); +} + +TEST_F(UnionTablePropertiesTest, noGlobalPhaseTwoQubitsB) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(hOp, q1, q5, q4, q6); + ut.propagateMeasurement(v5, v7, i0); + + const auto globalPhase0 = ut.globalPhaseThatIsAdded(zOp, v6, q7); + + ASSERT_FALSE(globalPhase0.has_value()); +} + +TEST_F(UnionTablePropertiesTest, globalPhaseTwoQubits) { + ut.propagateQubitAlloc(v3); + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q2, q5); + ut.propagateGate(xOp, q3, q6); + + const auto globalPhase0 = ut.globalPhaseThatIsAdded(zOp, v1, q4); + const auto globalPhase1 = ut.globalPhaseThatIsAdded(zOp, v5, q6); + + ASSERT_TRUE(globalPhase0.has_value()); + ASSERT_TRUE(globalPhase1.has_value()); + ASSERT_EQ(0.0, globalPhase0); + ASSERT_EQ(std::numbers::pi, globalPhase1); +} + +TEST_F(UnionTablePropertiesTest, findNonSatisfiableCombinationsA) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5); + ut.propagateGate(xOp, q5, q6, q4, q7); + std::vector combinations = {v6, v7}; + + ASSERT_FALSE(ut.areThereSatisfiableCombinations(combinations)); +} + +TEST_F(UnionTablePropertiesTest, findNonSatisfiableCombinationsB) { + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + std::vector combinations = {v5, v6}; + + ASSERT_TRUE(ut.areThereSatisfiableCombinations(combinations)); +} + +TEST_F(UnionTablePropertiesTest, findNonSatisfiableCombinationsC) { + ut.propagateIntAlloc(i1, 2); + ut.propagateGate(hOp, q0, q4); + ut.propagateGate(xOp, q1, q5, q4, q6); + ut.propagateMeasurement(v6, v7, i0); + ut.propagateMeasurement(v5, v8, i1); + ut.propagateGate(hOp, q7, q9); + ut.propagateGate(hOp, q8, q10, q9, q11); + + std::vector qubitCombinations = {v10, v11}; + std::vector classicalCombinations = {i0, i1}; + std::vector classicalVal0 = {i0}; + std::vector classicalVal1 = {i1}; + + ASSERT_TRUE(ut.areThereSatisfiableCombinations(qubitCombinations, + classicalCombinations)); + ASSERT_FALSE( + ut.areThereSatisfiableCombinations({}, classicalVal0, classicalVal1)); + ASSERT_TRUE(ut.areThereSatisfiableCombinations(qubitCombinations, {}, + classicalCombinations)); +} + +class SmallUnionTableTest : public testing::Test { +protected: + mlir::MLIRContext context; + QCOProgramBuilder programBuilder; + UnionTable ut = UnionTable(2, 2); + + HOp hOp; + XOp xOp; + + mlir::Value v0; + mlir::Value v1; + mlir::Value v2; + mlir::Value v3; + mlir::Value v4; + mlir::Value v5; + mlir::Value v6; + mlir::Value v7; + mlir::Value v8; + mlir::Value v9; + + std::vector q0; + std::vector q1; + std::vector q2; + std::vector q3; + std::vector q4; + std::vector q5; + std::vector q6; + std::vector q7; + std::vector q8; + std::vector q9; + + SmallUnionTableTest() : programBuilder(&context) {} + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.initialize(); + + auto q = programBuilder.allocQubitRegister(10); + hOp = HOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + xOp = XOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + + v0 = q[0]; + v1 = q[1]; + v2 = q[2]; + v3 = q[3]; + v4 = q[4]; + v5 = q[5]; + v6 = q[6]; + v7 = q[7]; + v8 = q[8]; + v9 = q[9]; + + q0 = {v0}; + q1 = {v1}; + q2 = {v2}; + q3 = {v3}; + q4 = {v4}; + q5 = {v5}; + q6 = {v6}; + q7 = {v7}; + q8 = {v8}; + q9 = {v9}; + + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + ut.propagateQubitAlloc(v2); + ut.propagateQubitAlloc(v3); + } +}; + +TEST_F(SmallUnionTableTest, handleErrorIfTwoManyAmplitudesAreNonzero) { + ut.propagateGate(hOp, q3, q4); + ut.propagateGate(xOp, q2, q5, q4, q6); + ut.propagateGate(hOp, q5, q7); + ut.propagateGate(hOp, q7, q8); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 32, HybridStates: {TOP}")); +} + +TEST_F(SmallUnionTableTest, applyGatesOnPartiallyTopQState) { + ut.propagateGate(hOp, q2, q4); + ut.propagateGate(hOp, q3, q5); + ut.propagateGate(xOp, q4, q6, q5, q7); // Qubit 2 and 3 enter TOP + ut.propagateGate(xOp, q1, q8, q6, q9); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 321, HybridStates: {TOP}")); +} + +TEST_F(SmallUnionTableTest, nonSuperfluousBecauseStatesAreTop) { + ut.propagateGate(hOp, q2, q4); + ut.propagateGate(hOp, q3, q5); + ut.propagateGate(xOp, q4, q6, q5, q7); // Qubit 2 and 3 enter TOP + std::vector quantumCtrl = {v7}; + auto [completelySuperfluous, superfluousQubits, superfluousClassicalValues] = + ut.getSuperfluousControls(quantumCtrl); + + EXPECT_FALSE(completelySuperfluous); + EXPECT_TRUE(superfluousQubits.empty()); +} + +class UnionTableSuperfluousTest : public testing::Test { +protected: + mlir::MLIRContext context; + QCOProgramBuilder programBuilder; + UnionTable ut = UnionTable(8, 4); + + HOp hOp; + XOp xOp; + ZOp zOp; + + mlir::Value v0; + mlir::Value v1; + mlir::Value v2; + mlir::Value v3; + mlir::Value v4; + mlir::Value v5; + mlir::Value v6; + mlir::Value v7; + mlir::Value v8; + mlir::Value v9; + mlir::Value v10; + mlir::Value v11; + mlir::Value v12; + mlir::Value v13; + mlir::Value v14; + mlir::Value v15; + mlir::Value v16; + mlir::Value v17; + mlir::Value v18; + mlir::Value i0; + mlir::Value i1; + mlir::Value i2; + mlir::Value i3; + + std::vector q0; + std::vector q1; + std::vector q2; + std::vector q3; + std::vector q4; + std::vector q5; + std::vector q6; + std::vector q7; + std::vector q8; + std::vector q9; + std::vector q10; + std::vector q11; + std::vector q12; + std::vector q13; + std::vector q14; + std::vector q15; + std::vector q16; + std::vector q17; + std::vector q18; + + UnionTableSuperfluousTest() : programBuilder(&context) {} + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.initialize(); + + auto q = programBuilder.allocQubitRegister(17); + hOp = HOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + xOp = XOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + zOp = ZOp::create(programBuilder, programBuilder.getLoc(), q[0].getType(), + q[0]); + + v0 = q[0]; + v1 = q[1]; + v2 = q[2]; + v3 = q[3]; + v4 = q[4]; + v5 = q[5]; + v6 = q[6]; + v7 = q[7]; + v8 = q[8]; + v9 = q[9]; + v10 = q[10]; + v11 = q[11]; + v12 = q[12]; + v13 = q[13]; + v14 = q[14]; + v15 = q[15]; + v16 = q[16]; + + q0 = {v0}; + q1 = {v1}; + q2 = {v2}; + q3 = {v3}; + q4 = {v4}; + q5 = {v5}; + q6 = {v6}; + q7 = {v7}; + q8 = {v8}; + q9 = {v9}; + q10 = {v10}; + q11 = {v11}; + q12 = {v12}; + q13 = {v13}; + q14 = {v14}; + q15 = {v15}; + q16 = {v16}; + + const auto iAttr = programBuilder.getI64IntegerAttr(0); + + i0 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + i1 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + i2 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + i3 = mlir::arith::ConstantOp::create(programBuilder, + programBuilder.getLoc(), iAttr); + + ut.propagateQubitAlloc(v0); + ut.propagateQubitAlloc(v1); + ut.propagateQubitAlloc(v2); + ut.propagateQubitAlloc(v3); + ut.propagateIntAlloc(i0, 10); + ut.propagateIntAlloc(i1, 10); + ut.propagateIntAlloc(i2, 10); + ut.propagateIntAlloc(i3, 10); + ut.propagateGate(hOp, q0, q4); + ut.propagateMeasurement(v4, v5, i0); + ut.propagateGate(hOp, q5, q6); + ut.propagateMeasurement(v6, v7, i1); + ut.propagateGate(hOp, q7, q8); + + ut.propagateGate(hOp, q1, q9); + ut.propagateGate(hOp, q9, q10); + ut.propagateMeasurement(v10, v11, i2); // classical value 2 = false + + ut.propagateGate(hOp, q2, q12); + + ut.propagateGate(hOp, q3, q13); + ut.propagateGate(zOp, q13, q14); + ut.propagateGate(hOp, q14, q15); + ut.propagateMeasurement(v15, v16, i3); // classical value 3 = true + } +}; + +TEST_F(UnionTableSuperfluousTest, oneSuperfluousEach) { + std::vector quantumCtrl = {v12, v16}; + std::vector posClassicalCtrl = {i0, i3}; + std::vector negClassicalCtrl = {i1, i2}; + auto [completelySuperfluous, superfluousQubits, superfluousClassicalValues] = + ut.getSuperfluousControls(quantumCtrl, posClassicalCtrl, + negClassicalCtrl); + ASSERT_EQ(superfluousQubits.size(), 1); + ASSERT_EQ(superfluousClassicalValues.size(), 2); + ASSERT_TRUE(superfluousQubits.contains(v16)); + ASSERT_TRUE(superfluousClassicalValues.contains(i2)); + ASSERT_TRUE(superfluousClassicalValues.contains(i3)); + ASSERT_FALSE(completelySuperfluous); +} + +TEST_F(UnionTableSuperfluousTest, completelySuperfluousDueToNegQuantumCtrl) { + std::vector quantumCtrl = {v11, v12, v16}; + std::vector posClassicalCtrl = {i0, i3}; + std::vector negClassicalCtrl = {i1, i2}; + const auto results = ut.getSuperfluousControls(quantumCtrl, posClassicalCtrl, + negClassicalCtrl); + ASSERT_TRUE(results.completelySuperfluous); +} + +TEST_F(UnionTableSuperfluousTest, completelySuperfluousDueToNegClassicalCtrl) { + std::vector quantumCtrl = {v12, v16}; + std::vector posClassicalCtrl = {i0, i2, i3}; + std::vector negClassicalCtrl = {i1}; + const auto results = ut.getSuperfluousControls(quantumCtrl, posClassicalCtrl, + negClassicalCtrl); + ASSERT_TRUE(results.completelySuperfluous); +} + +TEST_F(UnionTableSuperfluousTest, completelySuperfluousDueToPosClassicalCtrl) { + std::vector quantumCtrl = {v12, v16}; + std::vector posClassicalCtrl = {i0}; + std::vector negClassicalCtrl = {i1, i2, i3}; + const auto results = ut.getSuperfluousControls(quantumCtrl, posClassicalCtrl, + negClassicalCtrl); + ASSERT_TRUE(results.completelySuperfluous); +} +} // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_constant_propagation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_constant_propagation.cpp new file mode 100644 index 0000000000..1c72902b0e --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_constant_propagation.cpp @@ -0,0 +1,1005 @@ +/* + * 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/Transforms/Passes.h" +#include "mlir/Support/IRVerification.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +using namespace mlir; +using namespace mlir::qco; + +class QCOConstantPropagationTest : public testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder programBuilder; + QCOProgramBuilder referenceBuilder; + OwningOpRef module; + OwningOpRef reference; + + QCOConstantPropagationTest() + : programBuilder(&context), referenceBuilder(&context) {} + + void SetUp() override { + // Register all necessary dialects + DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.initialize(); + referenceBuilder.initialize(); + } + + /** + * @brief Adds the ConstantPropagation pass to the pass manager and runs it. + */ + static LogicalResult runConstantPropagationPass(ModuleOp module) { + verify(module); + PassManager pm(module.getContext()); + pm.addPass(createConstantPropagation()); + return pm.run(module); + } +}; + +} // namespace + +/** + * @brief Test: This test checks if CNOTs or the controls of CNOTs are removed + * if we can classically determine the ctrls value. + */ +TEST_F(QCOConstantPropagationTest, reducePosCtrls) { + const auto iAttr = programBuilder.getF64FloatAttr(-0.3926991); + Value i0 = + arith::ConstantOp::create(programBuilder, programBuilder.getLoc(), iAttr); + auto q = programBuilder.allocQubitRegister(4); + q[0] = programBuilder.h(q[0]); + q[0] = programBuilder.x(q[0]); + q[0] = programBuilder.h(q[0]); + programBuilder.crx(i0, q[0], q[1]); + q[2] = programBuilder.h(q[2]); + q[2] = programBuilder.z(q[2]); + q[2] = programBuilder.h(q[2]); + auto [q2, q3] = programBuilder.crx(i0, q[2], q[3]); + programBuilder.cry(0.3, q2, q3); + module = programBuilder.finalize(); + + const auto iAttrRef = referenceBuilder.getF64FloatAttr(-0.3926991); + Value i0Ref = arith::ConstantOp::create(referenceBuilder, + referenceBuilder.getLoc(), iAttrRef); + auto qRef = referenceBuilder.allocQubitRegister(4); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[0] = referenceBuilder.x(qRef[0]); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[2] = referenceBuilder.h(qRef[2]); + qRef[2] = referenceBuilder.z(qRef[2]); + qRef[2] = referenceBuilder.h(qRef[2]); + qRef[3] = referenceBuilder.rx(i0Ref, qRef[3]); + referenceBuilder.ry(0.3, qRef[3]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that CNOTs are not changed if the control is + * not in |0> or |1>. + */ +TEST_F(QCOConstantPropagationTest, testDontRemoveIfControlInSuperposition) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.h(q[0]); + programBuilder.cx(q[0], q[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.h(qRef[0]); + referenceBuilder.cx(qRef[0], qRef[1]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that an uncontrolled GPhase gate does not have + * an effect. + */ +TEST_F(QCOConstantPropagationTest, testApplyUncontrolledGPhase) { + auto q = programBuilder.allocQubitRegister(2); + const auto iAttr = programBuilder.getF64FloatAttr(1.0); + Value i0 = + arith::ConstantOp::create(programBuilder, programBuilder.getLoc(), iAttr); + q[0] = programBuilder.h(q[0]); + programBuilder.gphase(i0); + q[0] = programBuilder.h(q[0]); + programBuilder.cx(q[0], q[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + const auto iAttrRef = referenceBuilder.getF64FloatAttr(1.0); + Value i0Ref = arith::ConstantOp::create(referenceBuilder, + referenceBuilder.getLoc(), iAttrRef); + qRef[0] = referenceBuilder.h(qRef[0]); + referenceBuilder.gphase(i0Ref); + qRef[0] = referenceBuilder.h(qRef[0]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that a controlled GPhase gate has an effect. + */ +TEST_F(QCOConstantPropagationTest, testApplyControlledGPhase) { + auto q = programBuilder.allocQubitRegister(2); + const auto iAttr = programBuilder.getF64FloatAttr(1.0); + Value i0 = + arith::ConstantOp::create(programBuilder, programBuilder.getLoc(), iAttr); + q[0] = programBuilder.h(q[0]); + q[1] = programBuilder.h(q[1]); + q[0] = programBuilder.cgphase(i0, q[0]); + q[0] = programBuilder.h(q[0]); + programBuilder.cx(q[0], q[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + const auto iAttrRef = referenceBuilder.getF64FloatAttr(1.0); + Value i0Ref = arith::ConstantOp::create(referenceBuilder, + referenceBuilder.getLoc(), iAttrRef); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[1] = referenceBuilder.h(qRef[1]); + qRef[0] = referenceBuilder.cgphase(i0Ref, qRef[0]); + qRef[0] = referenceBuilder.h(qRef[0]); + referenceBuilder.cx(qRef[0], qRef[1]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that CNOTs are not changed if a reset is + * between two Hadamards, i.e. the qubits are in a superposition after the + * second Hadamard. + */ +TEST_F(QCOConstantPropagationTest, testApplyReset) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.h(q[0]); + q[0] = programBuilder.reset(q[0]); + q[0] = programBuilder.h(q[0]); + programBuilder.cx(q[0], q[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[0] = referenceBuilder.reset(qRef[0]); + qRef[0] = referenceBuilder.h(qRef[0]); + referenceBuilder.cx(qRef[0], qRef[1]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that implied Qubits are removed from a + * controlled gate. + */ +TEST_F(QCOConstantPropagationTest, testRemoveImpliedQubits) { + auto q = programBuilder.allocQubitRegister(5); + const auto iAttr = programBuilder.getF64FloatAttr(-0.3926991); + Value i0 = + arith::ConstantOp::create(programBuilder, programBuilder.getLoc(), iAttr); + q[0] = programBuilder.h(q[0]); + q[1] = programBuilder.h(q[1]); + auto [q01, q2] = + programBuilder.ctrl({q[0], q[1]}, {q[2]}, [&](const ValueRange target) { + return SmallVector{programBuilder.x(target[0])}; + }); + q[4] = programBuilder.x(q[4]); + auto [q124, q3] = programBuilder.ctrl( + {q01[1], q2[0], q[4]}, {q[3]}, [&](const ValueRange target) { + return SmallVector{programBuilder.rx(i0, target[0])}; + }); + programBuilder.h(q01[0]); + programBuilder.h(q124[0]); + programBuilder.h(q124[1]); + programBuilder.h(q3[0]); + programBuilder.h(q124[2]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(5); + const auto iAttrRef = referenceBuilder.getF64FloatAttr(-0.3926991); + Value i0Ref = arith::ConstantOp::create(referenceBuilder, + referenceBuilder.getLoc(), iAttrRef); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[1] = referenceBuilder.h(qRef[1]); + auto [qRef01, qRef2] = referenceBuilder.ctrl( + {qRef[0], qRef[1]}, {qRef[2]}, [&](const ValueRange target) { + return SmallVector{referenceBuilder.x(target[0])}; + }); + qRef[4] = referenceBuilder.x(qRef[4]); + auto [qRef21, qRef31] = referenceBuilder.crx(i0Ref, qRef2[0], qRef[3]); + referenceBuilder.h(qRef01[0]); + referenceBuilder.h(qRef01[1]); + referenceBuilder.h(qRef21); + referenceBuilder.h(qRef31); + referenceBuilder.h(qRef[4]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that gates whose quantum controls cannot be + * satisfied are removed. + */ +TEST_F(QCOConstantPropagationTest, testUnsatisfiableQuantumCombination) { + auto q = programBuilder.allocQubitRegister(3); + q[0] = programBuilder.h(q[0]); + q[1] = programBuilder.x(q[1]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + programBuilder.ctrl({q0, q1}, {q[2]}, [&](const ValueRange target) { + return SmallVector{programBuilder.s(target[0])}; + }); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(3); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[1] = referenceBuilder.x(qRef[1]); + referenceBuilder.cx(qRef[0], qRef[1]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that gates whose quantum and classical controls + * cannot be satisfied are removed. + */ +TEST_F(QCOConstantPropagationTest, testUnsatisfiableHybridCombination) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.h(q[0]); + q[1] = programBuilder.x(q[1]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + auto [q01, b0] = programBuilder.measure(q0); + const auto qRange01 = programBuilder.qcoIf( + b0, {q01, q1}, + [&](const ValueRange args) { + const auto [qi1, qi0] = programBuilder.ch(args[1], args[0]); + return SmallVector{qi0, qi1}; + }, + [&](const ValueRange args) { + const auto [qi0, qi1] = programBuilder.ch(args[0], args[1]); + return SmallVector{qi0, qi1}; + }); + programBuilder.y(qRange01[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[1] = referenceBuilder.x(qRef[1]); + auto [qRef0, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + referenceBuilder.measure(qRef0); + referenceBuilder.y(qRef1); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that gates are unconditionally applied if the + * bit they depend on is always zero. + */ +TEST_F(QCOConstantPropagationTest, testRemoveClassicalConditionalIfItsZero) { + auto q = programBuilder.allocQubitRegister(1); + q[0] = programBuilder.h(q[0]); + q[0] = programBuilder.h(q[0]); + auto [q0, b0] = programBuilder.measure(q[0]); + programBuilder.qcoIf( + b0, {q0}, + [&](const ValueRange args) { + const auto qi0 = programBuilder.x(args[0]); + return SmallVector{qi0}; + }, + [&](const ValueRange args) { + const auto qi0 = programBuilder.h(args[0]); + return SmallVector{qi0}; + }); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(1); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[0] = referenceBuilder.h(qRef[0]); + auto [qRef0, bRef0] = referenceBuilder.measure(qRef[0]); + referenceBuilder.h(qRef0); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that gates are unconditionally applied if the + * bit they depend on is always one. + */ +TEST_F(QCOConstantPropagationTest, testRemoveClassicalConditionalIfItsOne) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.x(q[0]); + auto [q0, b0] = programBuilder.measure(q[0]); + const auto qRange01 = programBuilder.qcoIf( + b0, {q0, q[1]}, + [&](const ValueRange args) { + const auto qi0 = programBuilder.h(args[0]); + const auto qi1 = programBuilder.h(args[1]); + const auto qi11 = programBuilder.z(qi1); + const auto [qi2, qi3] = programBuilder.cx(qi0, qi11); + return SmallVector{qi2, qi3}; + }, + [&](const ValueRange args) { + const auto qi0 = programBuilder.h(args[0]); + return SmallVector{qi0, args[1]}; + }); + programBuilder.h(qRange01[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.x(qRef[0]); + auto [qRef0, bRef0] = referenceBuilder.measure(qRef[0]); + qRef[0] = referenceBuilder.h(qRef0); + qRef[1] = referenceBuilder.h(qRef[1]); + qRef[1] = referenceBuilder.z(qRef[1]); + const auto [qRef01, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + referenceBuilder.h(qRef1); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks for various gates if they are removed if the + * classical conditional is true. + */ +TEST_F(QCOConstantPropagationTest, testRemoveClassicalConditionals) { + auto q = programBuilder.allocQubitRegister(1); + const auto bTrue = arith::ConstantOp::create(programBuilder, + programBuilder.getBoolAttr(true)) + .getResult(); + const auto bFalse = arith::ConstantOp::create( + programBuilder, programBuilder.getBoolAttr(false)) + .getResult(); + auto b = arith::OrIOp::create(programBuilder, bTrue.getType(), bTrue, bFalse) + .getResult(); + q[0] = programBuilder.h(q[0]); + programBuilder.qcoIf(b, {q[0]}, [&](const ValueRange args) { + const auto qi0 = programBuilder.u2(1.4, 2.7, args[0]); + const auto qi1 = programBuilder.sdg(qi0); + const auto qi2 = programBuilder.t(qi1); + const auto qi3 = programBuilder.sx(qi2); + const auto qi4 = programBuilder.tdg(qi3); + const auto qi5 = programBuilder.sxdg(qi4); + const auto qi6 = programBuilder.r(0.2, 0.4, qi5); + return SmallVector{qi6}; + }); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(1); + const auto bTrueRef = + arith::ConstantOp::create(referenceBuilder, + referenceBuilder.getBoolAttr(true)) + .getResult(); + const auto bFalseRef = + arith::ConstantOp::create(referenceBuilder, + referenceBuilder.getBoolAttr(false)) + .getResult(); + arith::OrIOp::create(referenceBuilder, bTrueRef.getType(), bTrueRef, + bFalseRef) + .getResult(); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[0] = referenceBuilder.u2(1.4, 2.7, qRef[0]); + qRef[0] = referenceBuilder.sdg(qRef[0]); + qRef[0] = referenceBuilder.t(qRef[0]); + qRef[0] = referenceBuilder.sx(qRef[0]); + qRef[0] = referenceBuilder.tdg(qRef[0]); + qRef[0] = referenceBuilder.sxdg(qRef[0]); + qRef[0] = referenceBuilder.r(0.2, 0.4, qRef[0]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if classical integer and float operations are + * applied without error. + */ +TEST_F(QCOConstantPropagationTest, testApplyClassicalOperations) { + auto q = programBuilder.allocQubitRegister(1); + const auto floatSeven = programBuilder.floatConstant(7.2); + const auto floatZero = programBuilder.floatConstant(0.0); + arith::MulFOp::create(programBuilder, floatSeven.getType(), floatSeven, + floatZero) + .getResult(); + const auto intTwo = programBuilder.intConstant(2); + const auto intTen = programBuilder.intConstant(10); + arith::AddIOp::create(programBuilder, intTwo.getType(), intTwo, intTen) + .getResult(); + module = programBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); +} + +/** + * @brief Test: This test checks that conditionals are not changed if we cannot + * tell the bits value. + */ +TEST_F(QCOConstantPropagationTest, testDoNotRemoveClassicalConditional) { + auto q = programBuilder.allocQubitRegister(1); + q[0] = programBuilder.h(q[0]); + auto [q0, b0] = programBuilder.measure(q[0]); + programBuilder.qcoIf( + b0, {q0}, + [&](const ValueRange args) { + const auto qi0 = programBuilder.x(args[0]); + return SmallVector{qi0}; + }, + [&](const ValueRange args) { + const auto qi0 = programBuilder.h(args[0]); + return SmallVector{qi0}; + }); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(1); + qRef[0] = referenceBuilder.h(qRef[0]); + auto [qRef0, bRef0] = referenceBuilder.measure(qRef[0]); + referenceBuilder.qcoIf( + bRef0, {qRef0}, + [&](const ValueRange args) { + const auto qi0 = referenceBuilder.x(args[0]); + return SmallVector{qi0}; + }, + [&](const ValueRange args) { + const auto qi0 = referenceBuilder.h(args[0]); + return SmallVector{qi0}; + }); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that a quantum conditional is replaced by a + * classical if a qubit and a classical bit are equivalent. + */ +TEST_F(QCOConstantPropagationTest, + testEquivalentPositiveClassicalAndQuantumControl) { + auto q = programBuilder.allocQubitRegister(3); + q[0] = programBuilder.h(q[0]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + programBuilder.measure(q0); + auto [q11, q2] = programBuilder.cx(q1, q[2]); + programBuilder.h(q11); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(3); + qRef[0] = referenceBuilder.h(qRef[0]); + auto [qRef0, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + auto [qRef01, bRef0] = referenceBuilder.measure(qRef0); + referenceBuilder.qcoIf(bRef0, {qRef[2]}, [&](const ValueRange args) { + const auto qi0 = referenceBuilder.x(args[0]); + return SmallVector{qi0}; + }); + referenceBuilder.h(qRef1); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that multiple quantum conditionals are replaced + * by a classical if a qubit and a classical bit are equivalent. + */ +TEST_F(QCOConstantPropagationTest, testEquivalentClassicalAndQuantumControl) { + auto q = programBuilder.allocQubitRegister(3); + q[0] = programBuilder.h(q[0]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + auto [q01, b0] = programBuilder.measure(q0); + auto [q11, q2] = programBuilder.cx(q1, q[2]); + q[1] = programBuilder.x(q11); + auto [q12, q21] = programBuilder.cy(q[1], q2); + programBuilder.x(q01); + programBuilder.y(q12); + programBuilder.h(q21); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(3); + qRef[0] = referenceBuilder.h(qRef[0]); + auto [qRef0, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + auto [qRef01, bRef0] = referenceBuilder.measure(qRef0); + const auto qRange2 = + referenceBuilder.qcoIf(bRef0, {qRef[2]}, [&](const ValueRange args) { + const auto qi0 = referenceBuilder.x(args[0]); + return SmallVector{qi0}; + }); + qRef[1] = referenceBuilder.x(qRef1); + const auto qRange21 = referenceBuilder.qcoIf( + bRef0, qRange2, + [&](const ValueRange args) { return SmallVector{args[0]}; }, + [&](const ValueRange args) { + const auto qi0 = referenceBuilder.y(args[0]); + return SmallVector{qi0}; + }); + referenceBuilder.x(qRef01); + referenceBuilder.y(qRef[1]); + referenceBuilder.h(qRange21[0]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that one of multiple quantum controls are + * replaced by a classical control. + */ +TEST_F(QCOConstantPropagationTest, testRemoveSomeQuantumControl) { + auto q = programBuilder.allocQubitRegister(3); + q[0] = programBuilder.h(q[0]); + q[1] = programBuilder.h(q[1]); + auto [q0, b0] = programBuilder.measure(q[0]); + auto [q01, q2] = + programBuilder.ctrl({q0, q[1]}, {q[2]}, [&](const ValueRange target) { + return SmallVector{programBuilder.x(target[0])}; + }); + programBuilder.x(q01[0]); + programBuilder.y(q01[1]); + programBuilder.z(q2[0]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(3); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[1] = referenceBuilder.h(qRef[1]); + auto [qRef01, bRef0] = referenceBuilder.measure(qRef[0]); + const auto qRangeRef = referenceBuilder.qcoIf( + bRef0, {qRef[1], qRef[2]}, [&](const ValueRange args) { + const auto [qi0, qi1] = referenceBuilder.cx(args[0], args[1]); + return SmallVector{qi0, qi1}; + }); + referenceBuilder.x(qRef01); + referenceBuilder.y(qRangeRef[0]); + referenceBuilder.z(qRangeRef[1]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that multiple quantum controls are replaced by + * multiple classical controls. + */ +TEST_F(QCOConstantPropagationTest, + testReplaceQuantumCtrlsByMultipleClassicalCtrls) { + auto q = programBuilder.allocQubitRegister(3); + q[0] = programBuilder.h(q[0]); + q[1] = programBuilder.h(q[1]); + auto [q0, b0] = programBuilder.measure(q[0]); + auto [q1, b1] = programBuilder.measure(q[1]); + q[1] = programBuilder.x(q1); + programBuilder.ctrl({q0, q[1]}, {q[2]}, [&](const ValueRange target) { + return SmallVector{programBuilder.x(target[0])}; + }); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(3); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[1] = referenceBuilder.h(qRef[1]); + auto [qRef0, bRef0] = referenceBuilder.measure(qRef[0]); + auto [qRef1, bRef1] = referenceBuilder.measure(qRef[1]); + qRef[1] = referenceBuilder.x(qRef1); + const auto zero = arith::ConstantOp::create( + referenceBuilder, referenceBuilder.getBoolAttr(false)) + .getResult(); + const auto negated1 = + arith::CmpIOp::create(referenceBuilder, bRef1.getType(), + arith::CmpIPredicate::eq, bRef1, zero) + .getResult(); + const auto andOp = + arith::AndIOp::create(referenceBuilder, bRef0.getType(), bRef0, negated1) + .getResult(); + referenceBuilder.qcoIf(andOp, {qRef[2]}, [&](const ValueRange target) { + return SmallVector{referenceBuilder.x(target[0])}; + }); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if a quantum control is removed if the + * classical control implies the quantum one. + */ +TEST_F(QCOConstantPropagationTest, testClassicalImpliesQuantum) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.h(q[0]); + auto [q0, b0] = programBuilder.measure(q[0]); + q[1] = programBuilder.x(q[1]); + auto [q01, q1] = programBuilder.cx(q0, q[1]); + auto [q11, q02] = programBuilder.ch(q1, q01); + const auto qRange = + programBuilder.qcoIf(b0, {q02, q11}, [&](const ValueRange args) { + const auto [qi0, qi1] = programBuilder.cx(args[0], args[1]); + return SmallVector{qi0, qi1}; + }); + programBuilder.x(qRange[0]); + programBuilder.y(qRange[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.h(qRef[0]); + auto [qRef0, bRef0] = referenceBuilder.measure(qRef[0]); + qRef[1] = referenceBuilder.x(qRef[1]); + const auto qRefRange1 = + referenceBuilder.qcoIf(bRef0, {qRef[1]}, [&](const ValueRange args) { + const auto qi0 = referenceBuilder.x(args[0]); + return SmallVector{qi0}; + }); + const auto qRefRange0 = referenceBuilder.qcoIf( + bRef0, {qRef0}, + [&](const ValueRange args) { return SmallVector{args[0]}; }, + [&](const ValueRange args) { + const auto qi0 = referenceBuilder.h(args[0]); + return SmallVector{qi0}; + }); + const auto qRefRange = referenceBuilder.qcoIf( + bRef0, {qRefRange0[0], qRefRange1[0]}, [&](const ValueRange args) { + const auto qi0 = referenceBuilder.x(args[1]); + return SmallVector{args[0], qi0}; + }); + referenceBuilder.x(qRefRange[0]); + referenceBuilder.y(qRefRange[1]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if propagation through classical branching is + * done correctly. + */ +TEST_F(QCOConstantPropagationTest, testPropagatingThroughClassicalBranching) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.h(q[0]); + q[1] = programBuilder.x(q[1]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + auto [q01, b0] = programBuilder.measure(q0); + const auto qRange = programBuilder.qcoIf( + b0, {q01, q1}, + [&](const ValueRange args) { + const auto qubit = programBuilder.x(args[1]); + return SmallVector{args[0], qubit}; + }, + [&](const ValueRange args) { + const auto qubit = programBuilder.x(args[0]); + return SmallVector{qubit, args[1]}; + }); + programBuilder.cz(qRange[0], qRange[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[1] = referenceBuilder.x(qRef[1]); + auto [qRef0, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + auto [qRef01, bRef0] = referenceBuilder.measure(qRef0); + referenceBuilder.qcoIf( + bRef0, {qRef01, qRef1}, + [&](const ValueRange args) { + const auto qubit = referenceBuilder.x(args[1]); + return SmallVector{args[0], qubit}; + }, + [&](const ValueRange args) { + const auto qubit = referenceBuilder.x(args[0]); + return SmallVector{qubit, args[1]}; + }); + referenceBuilder.gphase(std::numbers::pi); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if propagation through classical branching is + * done correctly if yield yields the values in a different order than the input + * order. + */ +TEST_F(QCOConstantPropagationTest, + testPropagatingThroughClassicalBranchingWithSwappedYieldedValues) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.h(q[0]); + q[1] = programBuilder.x(q[1]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + auto [q01, b0] = programBuilder.measure(q0); + const auto qRange = programBuilder.qcoIf( + b0, {q01, q1}, + [&](const ValueRange args) { + const auto qubit = programBuilder.x(args[1]); + return SmallVector{qubit, args[0]}; + }, + [&](const ValueRange args) { + const auto qubit = programBuilder.x(args[0]); + return SmallVector{args[1], qubit}; + }); + programBuilder.cx(qRange[0], qRange[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[1] = referenceBuilder.x(qRef[1]); + auto [qRef0, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + auto [qRef01, bRef0] = referenceBuilder.measure(qRef0); + const auto qRangeRef = referenceBuilder.qcoIf( + bRef0, {qRef01, qRef1}, + [&](const ValueRange args) { + const auto qubit = referenceBuilder.x(args[1]); + return SmallVector{qubit, args[0]}; + }, + [&](const ValueRange args) { + const auto qubit = referenceBuilder.x(args[0]); + return SmallVector{args[1], qubit}; + }); + referenceBuilder.x(qRangeRef[1]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if a phase gate is removed if it only adds a + * global phase = 1. + */ +TEST_F(QCOConstantPropagationTest, testReplaceSingleQubitPhaseGatePlusOne) { + auto q = programBuilder.allocQubitRegister(1); + q[0] = programBuilder.h(q[0]); + q[0] = programBuilder.z(q[0]); + q[0] = programBuilder.z(q[0]); + q[0] = programBuilder.h(q[0]); + q[0] = programBuilder.z(q[0]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(1); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[0] = referenceBuilder.z(qRef[0]); + qRef[0] = referenceBuilder.z(qRef[0]); + qRef[0] = referenceBuilder.h(qRef[0]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if a phase gate is replaced by a global phase + * gate if it only adds a global phase. + */ +TEST_F(QCOConstantPropagationTest, testReplaceSingleQubitPhaseGateMinusOne) { + auto q = programBuilder.allocQubitRegister(1); + q[0] = programBuilder.h(q[0]); + q[0] = programBuilder.z(q[0]); + q[0] = programBuilder.h(q[0]); + q[0] = programBuilder.z(q[0]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(1); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[0] = referenceBuilder.z(qRef[0]); + qRef[0] = referenceBuilder.h(qRef[0]); + referenceBuilder.gphase(std::numbers::pi); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if a multi-qubit phase gate is removed if it + * only adds a global phase that is one. + */ +TEST_F(QCOConstantPropagationTest, testRemoveMultiQubitPhaseGatePlusOne) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.h(q[0]); + programBuilder.cz(q[0], q[1]); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.h(qRef[0]); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if a multi-qubit phase gate is replaced if it + * only adds a global phase. + */ +TEST_F(QCOConstantPropagationTest, testRemoveMultiQubitPhaseGateMinusOne) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.x(q[0]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + programBuilder.cz(q0, q1); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + referenceBuilder.x(qRef[0]); + referenceBuilder.x(qRef[1]); + referenceBuilder.gphase(std::numbers::pi); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks if a multi-qubit phase gate is not removed if + * it adds different global phases depending on the actual state. + */ +TEST_F(QCOConstantPropagationTest, testDoNotRemoveMultiQubitPhaseGate) { + auto q = programBuilder.allocQubitRegister(2); + q[0] = programBuilder.h(q[0]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + programBuilder.cz(q0, q1); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(2); + qRef[0] = referenceBuilder.h(qRef[0]); + auto [qRef0, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + referenceBuilder.cz(qRef0, qRef1); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that a measurement is moved to front and the + * measurement result is afterward used as conditional. + */ +TEST_F(QCOConstantPropagationTest, testMoveMeasurementToFront) { + auto q = programBuilder.allocQubitRegister(3); + q[0] = programBuilder.h(q[0]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + auto [q11, q2] = programBuilder.cx(q1, q[2]); + programBuilder.h(q11); + programBuilder.measure(q0); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(3); + qRef[0] = referenceBuilder.h(qRef[0]); + auto [qRef0, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + auto [qRef01, bRef0] = referenceBuilder.measure(qRef0); + referenceBuilder.qcoIf(bRef0, {qRef[2]}, [&](const ValueRange args) { + const auto qi0 = referenceBuilder.x(args[0]); + return SmallVector{qi0}; + }); + referenceBuilder.h(qRef1); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +} + +/** + * @brief Test: This test checks that two measurements are moved to front. + */ +TEST_F(QCOConstantPropagationTest, testMoveTwoMeasurementToFront) { + auto q = programBuilder.allocQubitRegister(4); + q[0] = programBuilder.h(q[0]); + q[2] = programBuilder.h(q[2]); + auto [q0, q1] = programBuilder.cx(q[0], q[1]); + auto [q2, q3] = programBuilder.cx(q[2], q[3]); + programBuilder.h(q3); + programBuilder.measure(q0); + programBuilder.measure(q1); + module = programBuilder.finalize(); + + auto qRef = referenceBuilder.allocQubitRegister(4); + qRef[0] = referenceBuilder.h(qRef[0]); + qRef[2] = referenceBuilder.h(qRef[2]); + auto [qRef0, qRef1] = referenceBuilder.cx(qRef[0], qRef[1]); + referenceBuilder.measure(qRef0); + referenceBuilder.measure(qRef1); + auto [qRef2, qRef3] = referenceBuilder.cx(qRef[2], qRef[3]); + referenceBuilder.h(qRef3); + reference = referenceBuilder.finalize(); + + ASSERT_TRUE(runConstantPropagationPass(module.get()).succeeded()); + + EXPECT_TRUE( + areModulesEquivalentWithPermutations(module.get(), reference.get())); +}