diff --git a/CHANGELOG.md b/CHANGELOG.md index cd00940b0c..44faa08e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- ✨ Add a `constant propagation` pass for reducing superfluous quantum + resources by propagating the quantum machine state ([#1845]) ([**@lirem101]) - 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function ([#1826]) ([**@ystade]) - 🐳 Add dev container configuration for consistent local development @@ -594,6 +596,7 @@ changelogs._ +[#1845]: https://github.com/munich-quantum-toolkit/core/pull/1845 [#1842]: https://github.com/munich-quantum-toolkit/core/pull/1842 [#1830]: https://github.com/munich-quantum-toolkit/core/pull/1830 [#1828]: https://github.com/munich-quantum-toolkit/core/pull/1828 diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index 54d9f039de..0e47c99af8 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -49,6 +49,9 @@ struct QuantumCompilerConfig { /// Enable Hadamard lifting bool enableHadamardLifting = false; + + /// Enable constant propagation + bool enableConstantPropagation = false; }; /** 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..15d8f351f4 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h @@ -0,0 +1,109 @@ +/* + * 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 + +#ifndef MQT_CORE_CLASSICALARITHOPERATION_H +#define MQT_CORE_CLASSICALARITHOPERATION_H + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/Drivers.h" + +#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, + int64_t value1, int64_t value2 = 0, + int64_t value3 = 0) { + + for (mlir::Value operand : operation->getOperands()) { + if (isa(operand.getType())) { + throw std::runtime_error( + "Constant propagation does not support vectors as classical types."); + } + } + + return mlir::TypeSwitch(operation) + .Case([&](auto) { return value1 + value2; }) + .Case([&](auto) { return value1 & value2; }) + .Case([&](auto) { + // Division that rounds to positive infinity + return ceil(1.0 * value1 / value2); + }) + .Case([&](auto) { + // Division that rounds towards zero + return value1 / value2; + }) + .Case([&](auto) { + // Division that rounds to negative infinity + return floor(1.0 * value1 / value2); + }) + .Case( + [&](auto) { return value1 > value2 ? value1 : value2; }) + .Case( + [&](auto) { return value1 < value2 ? value1 : value2; }) + .Case([&](auto) { return value1 * value2; }) + .Case([&](auto) { return value1 | value2; }) + .Case( + [&](auto) { return remainder(value1, value2); }) + .Case([&](auto) { return value1 << value2; }) + .Case([&](auto) { return value1 >> value2; }) + .Case([&](auto) { return value1 - value2; }) + .Case([&](auto) { return value1 ^ value2; }) + .Case( + [&](auto) { return value1 == 0 ? value3 : value2; }) + .Default([&](auto) -> int64_t { + throw std::runtime_error("Unsupported integer operation in " + "mlir::qco::classicalarithoperation"); + }); +} + +inline double getArithDoubleOpResult(mlir::Operation* operation, double value1, + double value2 = 0.0) { + + for (mlir::Value operand : operation->getOperands()) { + if (isa(operand.getType())) { + throw std::runtime_error( + "Constant propagation does not support vectors as classical types."); + } + } + + return mlir::TypeSwitch(operation) + .Case([&](auto) { return value1 + value2; }) + .Case([&](auto) { return value1 / value2; }) + .Case( + [&](auto) { return value1 > value2 ? value1 : value2; }) + .Case( + [&](auto) { return value1 > value2 ? value1 : value2; }) + .Case( + [&](auto) { return value1 < value2 ? value1 : value2; }) + .Case( + [&](auto) { return value1 < value2 ? value1 : value2; }) + .Case([&](auto) { return value1 * value2; }) + .Case([&](auto) { return -value1; }) + .Case( + [&](auto) { return remainder(value1, value2); }) + .Case([&](auto) { return value1 - value2; }) + .Default([&](auto) -> double { + throw std::runtime_error("Unsupported floating-point operation in " + "mlir::qco::classicalarithoperation"); + }); +} + +#endif // MQT_CORE_MQT_CORE_CLASSICALARITHOPERATION_H 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..6a2326e6ba --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h @@ -0,0 +1,229 @@ +/* + * 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 +#ifndef MQT_CORE_GATETOMAP_H +#define MQT_CORE_GATETOMAP_H + +#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. + */ + +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, exp(Complex(0, params[0]))}}}, + {1, + {{0, -exp(Complex(0, params[1]))}, + {1, 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)}, {1, Complex(0, -s)}}}, + {1, {{0, Complex(0, -s)}, {1, Complex(c, 0)}}}, + {2, {{2, Complex(c, 0)}, {3, Complex(0, s)}}}, + {3, {{2, 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 { + throw std::runtime_error("Unsupported gate in mlir::qco::gatetomap"); + }); +} + +#endif // MQT_CORE_GATETOMAP_H 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..853b65bae7 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp @@ -0,0 +1,322 @@ +/* + * 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_HYBRIDSTATE_H +#define MQT_CORE_HYBRIDSTATE_H + +#include "QuantumState.hpp" + +#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. + * + * @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. + * @throws domain_error If a classical control value cannot be found. + */ + 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)) { + throw std::domain_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)) { + throw std::domain_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. + * + * @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. + * @throws domain_error If a classical control value cannot be found. + * @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. + * + * @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. + * @throws domain_error If a classical control value cannot be found. + */ + 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. + * + * @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. + * @throws domain_error If a classical control value cannot be found. + * @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. + * + * @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. + * @throws domain_error If a classical control value cannot be found. + * @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. + * @throws domain_error If a classical value cannot be found. + * @throws runtime_error If classical operation is not supported. + */ + 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, throws a domain_error. + * + * @param that The HybridState to unify this with. + * @throw std::domain_error If the unified QuantumState would exceed + * maxNonzeroAmplitudes of this. + */ + 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). + * + * @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. + * @throws domain_error If a classical value cannot be found. + * @returns True if the amplitude is always zero, false otherwise. + */ + [[nodiscard("HybridState::hasAlwaysZeroAmplitude 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 + +#endif // MQT_CORE_HYBRIDSTATE_H 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..db59ec1251 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp @@ -0,0 +1,349 @@ +/* + * 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_QUANTUMSTATE_H +#define MQT_CORE_QUANTUMSTATE_H +#include + +#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; + + 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( + 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 mapForThisQubit = gateMapping[mapFrom]; + for (unsigned int i = 0; i < numberOfTargetValues; i++) { + if (auto valueToI = mapForThisQubit[i]; 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; + } + } + + if (std::abs(1.0 - probabilityZero - probabilityOne) > 1e-4) { + throw std::domain_error( + "Probabilities of 0 and 1 do not add up to one after measurement."); + } + 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 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, throws a domain_error. + * + * @param that The QuantumState to unify this with. + * @throw std::domain_error If the number of nonzero amplitudes would exceed + * maxNonzeroAmplitudes of this. + */ + [[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 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, throws a domain_error. + * + * @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. + * @throw std::domain_error If the number of nonzero amplitudes would exceed + * maxNonzeroAmplitudes. + */ + 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 + +#endif // MQT_CORE_QUANTUMSTATE_H 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..d9208b0d7c --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp @@ -0,0 +1,547 @@ +/* + * 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_H +#define MQT_CORE_UNIONTABLE_H +#include "HybridState.hpp" + +#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 std::uint64_t nextId() { + static unsigned int counter = 0; + return ++counter; + } +}; + +/** + * @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 throws a domain_error. The same holds + * if the resulting hybridStates are more than maximumHybridEntries. + * + * @param entriesToUnify The UnionTableEntries to be unified. + * @throws domain_error If more than maNonzeroAmplitudes are created in a + * quantumstate or more than maximumHybridEntries are created. + */ + void unifyEntries(const std::set& entriesToUnify) { + if (entriesToUnify.size() == 1) { + return; + } + 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) { + numberOfNewEntries *= e.states.size(); + } + if (numberOfNewEntries > maximumHybridEntries) { + throw std::domain_error("Maximum of allowed hybrid entries exceeded."); + } + + // 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 (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); + } + + /** + * @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::ranges::reverse(newQuantumTargets); + replaceValuesGlobally(targets, newQuantumTargets); + } + + /** + * @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 wether 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 + * + * @param replacedValues Values to be replaced + * @param newValues Values the first values are replaced with. + * @throws runtime_error if the size of the two parameters is not equal. + */ + 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. + * + * @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. + * @throws invalid_argument if a value is given, but is not found in the + * existing ones. + */ + 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. + * + * + * @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. + * @throws invalid_argument if a value is given, but is not found in the + * existing ones. + */ + 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. + * + * @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. + * @throws invalid_argument if a value is given, but is not found in the + * existing ones. This does not hold for the classical target, which can be + * newly created. + */ + 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. + * + * @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. + * @throws invalid_argument if a value is given, but is not found in the + * existing ones. + */ + 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). + * + * @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. + * @throws invalid_argument if a value is given, but is not found in the + * existing ones. + * @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. + * + * @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. + * @throws invalid_argument if a value is given, but is not found in the + * existing ones. + * @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 + +#endif // MQT_CORE_UNIONTABLE_H diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index fafb906a77..0646a8c6be 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -200,4 +200,61 @@ def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> { }]; } +def ConstantPropagation : Pass<"constant-propagation", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect"]; + 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 routines 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 vales are considered + true if they are not zero. + If a controlling qubit or classical value is alaways 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", "maximumNonzeroAmplitudes", + "std::size_t", "4", + "The maximum number of non-zero amplitudes in the " + "tracted quantum states before reaching top.">, + Option<"maximumHybridStates", "maximumHybridStates", + "std::size_t", "4", + "The maximum number of hybrid states which have a " + "non-zero probability.">]; +} + #endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 821ccda2ee..06e1c594ad 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -153,6 +153,9 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, if (config_.enableHadamardLifting) { pm.addPass(qco::createHadamardLifting()); } + if (config_.enableConstantPropagation) { + pm.addPass(qco::createConstantPropagation()); + } }))) { return failure(); } 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..6005d744f5 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp @@ -0,0 +1,1073 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_CONSTANTPROPAGATION +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { + +#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()); \ + }) + +/** + * @brief Result of checking how do modify a controlled gate. + */ +struct controlsToModify { + llvm::DenseSet quantumCtrlsToRemove; + llvm::DenseSet classicalPosCtrlsToAdd; + llvm::DenseSet classicalNegCtrlsToAdd; +}; + +LogicalResult iterateThroughWorklist(PatternRewriter& rewriter, UnionTable* ut, + std::span& worklist, + const std::span posClassicalCtrls, + const 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. + */ +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 + */ +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(); + while (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. + */ +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. + */ +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. + */ +void inlineBranchBlock(IfOp* ifOperation, Block* block, + std::span& worklist, + PatternRewriter& rewriter) { + const auto 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. + */ +WalkResult handleConstant(UnionTable* ut, arith::ConstantOp op, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls) { + if (!posClassicalCtrls.empty() || !negClassicalCtrls.empty()) { + throw std::logic_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. + */ +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. + */ +Operation* +createOperationFromUnitaryOperation(Operation* op, PatternRewriter& rewriter, + const std::span qubitsIn) { + const auto 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([&](auto) -> Operation* { + throw std::runtime_error("Unsu" + "ppor" + "ted " + "oper" + "atio" + "n"); + }); + + 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. + */ +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()}; + const auto 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. + */ +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()) { + throw std::runtime_error("Cannot remove all controls of a CtrlOp"); + } + 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()}; + const auto 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. + */ +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; + + // propagate through then and else block + if (!thenEmpty) { + for (const Value arg : thenBlock->getArguments()) { + thenArgs.push_back(arg); + } + ut->replaceValuesGlobally(targets, thenArgs); + std::vector newWorklist; + + // Create a new worklist to iterate over the inner instructions + op->thenBlock()->walk([&](Operation* innerOp) { + newWorklist.push_back(innerOp); + std::ranges::replace(worklist, innerOp, static_cast(nullptr)); + }); + std::span wl = {newWorklist.begin(), newWorklist.end()}; + std::vector newPosClassicalCtrls = {posClassicalCtrls.begin(), + posClassicalCtrls.end()}; + newPosClassicalCtrls.push_back(condition); + const auto resThen = iterateThroughWorklist( + rewriter, ut, wl, newPosClassicalCtrls, negClassicalCtrls); + + if (resThen.failed()) { + return WalkResult::interrupt(); + } + 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) { + std::ranges::replace(thenArgs, input[i], output[i]); + } + }); + } + if (!elseEmpty) { + for (const Value arg : elseBlock->getArguments()) { + elseArgs.push_back(arg); + } + ut->replaceValuesGlobally(thenArgs.empty() ? targets : thenArgs, elseArgs); + std::vector newWorklist; + + op->elseBlock()->walk([&](Operation* innerOp) { + newWorklist.push_back(innerOp); + std::ranges::replace(worklist, innerOp, static_cast(nullptr)); + }); + std::span wl = {newWorklist.begin(), newWorklist.end()}; + 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(); + } + op->elseBlock()->walk([&](Operation* innerOp) { + // Propagating values in order to assign the right values to the right + // result values + const auto input = innerOp->getOperands(); + const auto output = innerOp->getResults(); + for (unsigned int i = 0; i < std::min(input.size(), output.size()); ++i) { + std::ranges::replace(elseArgs, input[i], output[i]); + } + }); + } + 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; + if (!thenArgs.empty()) { + 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); + } + } + } + if (!elseArgs.empty()) { + 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) { + throw std::runtime_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->getResults()[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(elseArgs.empty() ? 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. + */ +WalkResult putOperationIntoBranch(UnionTable* ut, UnitaryOpInterface op, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls, + controlsToModify ctrlsToMod, + PatternRewriter& rewriter, + std::span& worklist) { + const bool createThenBranch = !ctrlsToMod.classicalPosCtrlsToAdd.empty(); + const Value condition = createThenBranch + ? *ctrlsToMod.classicalPosCtrlsToAdd.begin() + : *ctrlsToMod.classicalNegCtrlsToAdd.begin(); + 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. + */ +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 { + throw std::invalid_argument( + "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. + */ +WalkResult handleUncontrolledUnitary(UnionTable* ut, UnitaryOpInterface* op, + const std::span posClassicalCtrls, + const std::span negClassicalCtrls, + PatternRewriter& rewriter, + std::span& worklist) { + 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. + */ +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); + const auto superfluousCtrls = ut->getSuperfluousControls( + inCtrlValues, posClassicalCtrls, negClassicalCtrls); + if (superfluousCtrls.completelySuperfluous || !satisfiable) { + 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); + } + + for (const auto qCtrl : inCtrlValues) { + 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.reserve(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[i] == body.getInputTarget(i)) { + targetQubits.insert(targetQubits.begin() + i, + op->getInputTarget(argIndex)); + break; + } + } + } + + std::vector resultQubits; + resultQubits.reserve(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.insert(resultQubits.begin() + 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. + */ +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. + } + // auto n = curr->getName().stripDialect().str(); + // std::string oName = + // "Op: " + curr->getName().getStringRef().str() + + // " dialect: " + curr->getName().getDialectNamespace().str(); + + 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)) { + throw std::domain_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(); + } + + throw std::runtime_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 + * tracted 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 + */ +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; + + for (const auto func : module.getOps()) { + + if (!isEntryPoint(func)) { + continue; // Ignore non entry_point functions for now. + } + func->walk( + [&](Operation* op) { worklist.push_back(op); }); + } + + // TODO: Take maximum from params + auto ut = UnionTable(maxNonzeroAmplitudes, maxHybridStates); + + std::span wl = {worklist.begin(), worklist.end()}; + + return iterateThroughWorklist(rewriter, &ut, wl, {}, {}); +} + +/** + * 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 + : impl::ConstantPropagationBase { + using ConstantPropagationBase::ConstantPropagationBase; + + void runOnOperation() override { + if (failed(applyCP(getOperation(), &getContext(), maximumNonzeroAmplitudes, + maximumHybridStates))) { + signalPassFailure(); + } + } +}; + +} // namespace + +} // namespace mlir::qco 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..5bb7bde127 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp @@ -0,0 +1,329 @@ +/* + * 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_HYBRIDSTATE +#define MQT_CORE_HYBRIDSTATE +#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 + +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::string str = "{" + this->qState->toString() + "}: "; + unsigned int i = 0; + bool first = true; + for (const auto& key : integerValues.keys()) { + if (!first) { + str += ", "; + } + first = false; + str += "integerValue" + std::to_string(i) + " = " + + std::to_string(integerValues.at(key)); + ++i; + } + unsigned int j = 0; + for (const auto& key : doubleValues.keys()) { + if (!first) { + str += ", "; + } + first = false; + str += "doubleValue" + std::to_string(j) + " = " + + std::format("{:.2f}", doubleValues.at(key)); + ++j; + } + if (i > 0 || j > 0) { + str += "; "; + } + str += "p = " + std::format("{:.2f}", this->probability) + ";"; + return str; +} + +bool HybridState::operator==(const HybridState& that) const { + if (top) { + return 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; + } + } + + for (const auto& [d, v] : doubleValues) { + if (!that.doubleValues.contains(d) || + std::fabs(that.doubleValues.at(d)) > 1e-4) { + return false; + } + } + + return true; +} + +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(integerValues.at(p)); + } else if (doubleValues.contains(p)) { + paramValues.push_back(doubleValues.at(p)); + } else { + throw std::domain_error( + "HybridState needs a classical value for gate parameters that is " + "not existent in current HybridState."); + } + } + try { + qState->propagateGate(gate, targets, ctrlsQuantum, paramValues); + } catch (std::domain_error const&) { + top = true; + } + } else { + try { + qState->propagateGate(gate, targets, ctrlsQuantum); + } catch (std::domain_error const&) { + 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)) { + throw std::domain_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)) { + throw std::domain_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(); + try { + newHybridState.qState = + std::make_shared(qState->unify(*that.qState)); + } catch (std::domain_error const&) { + 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::norm(doubleValues.at(v)) > 1e-4; + } + throw std::domain_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::norm(doubleValues.at(v)) < 1e-4; + if ((zeroDouble && i) || (!zeroDouble && !i)) { + return true; + } + } else { + throw std::domain_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::norm(d) < 1e-4) || + (qubitOne && std::norm(d) >= 1e-4)) { + result[v] = true; + } else { + result[v] = false; + } + } + return result; +} + +} // namespace mlir::qco + +#endif // MQT_CORE_HYBRIDSTATE 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..b8c0489502 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp @@ -0,0 +1,252 @@ +/* + * 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_QUANTUMSTATE +#define MQT_CORE_QUANTUMSTATE +#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 + +namespace mlir::qco { + +QuantumState::QuantumState(const std::span globalQubitNumber, + const std::size_t maxNonzeroAmplitudes) + : nQubits(globalQubitNumber.size()), + maxNonzeroAmplitudes(maxNonzeroAmplitudes) { + std::ranges::sort(globalQubitNumber); + unsigned int localQ = 0; + for (auto globalQ : globalQubitNumber) { + 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::string str; + bool first = true; + for (auto ordered = + std::map(this->amplitudeMap.begin(), this->amplitudeMap.end()); + auto const& [key, val] : ordered) { + if (!first) { + str += ", "; + } + first = false; + + str.push_back('|'); + str.append(qubitStringToBinary(key)); + str.append("> -> "); + + str.append(std::format("{:.2f}", val.real())); + + if (std::abs(val.imag()) > 1e-4) { + str.append(val.imag() > 0 ? " + i" : " - i"); + str.append(std::format("{:.2f}", std::abs(val.imag()))); + } + } + + return 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; + } +} + +QuantumState QuantumState::unify(const QuantumState& that) { + // Check if future state would be too large + if (amplitudeMap.size() * that.amplitudeMap.size() > maxNonzeroAmplitudes) { + throw std::domain_error("Number of nonzero amplitudes too high. State " + "needs to be treated as TOP."); + } + + 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::propagateGate(Operation* gate, + const std::span targets, + const std::span ctrls, + const std::span params) { + 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) { + throw std::domain_error("Number of nonzero amplitudes too high. State " + "needs to be treated as TOP."); + } +} + +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 { + 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 { + 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 { + 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 + +#endif // MQT_CORE_QUANTUMSTATE 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..729f53c61d --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp @@ -0,0 +1,661 @@ +/* + * 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/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#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()) { + throw std::domain_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) { + 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; + } + + try { + unifyEntries(participatingEntries); + } catch (std::domain_error&) { + putEntriesToTop(participatingEntries); + replaceValuesGlobally(targets, newQuantumTargets); + return; + } + if (valuesToEntries.at(targets[0])->top) { + replaceValuesGlobally(targets, newQuantumTargets); + 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(*targets.begin()); + 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); + + try { + unifyEntries(participatingEntries); + } catch (std::domain_error&) { + 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}; + try { + unifyEntries(participatingEntries); + } catch (std::domain_error&) { + 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.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}; + try { + unifyEntries(participatingEntries); + } catch (std::domain_error&) { + 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) { + try { + auto newStates = + hs.propagateReset(qubitsToGlobalIndices.at(quantumTarget), + posCtrlsClassical, negCtrlsClassical); + vecOfNewStates.insert(vecOfNewStates.end(), newStates.begin(), + newStates.end()); + } catch (std::domain_error&) { + putEntriesToTop({*ute}); + vecOfNewStates.clear(); + break; + } + } + ute->states = vecOfNewStates; + replaceValuesGlobally(quantumTargetVec, newQuantumValueVec); +} + +void UnionTable::propagateQubitAlloc(const Value qubit) { + unsigned int maxIndex = 0; + if (!qubitsToGlobalIndices.empty()) { + maxIndex = std::ranges::max(qubitsToGlobalIndices.values()) + 1; + } + 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); + entries.insert(std::make_shared(ute)); + valuesToEntries[intValue] = std::make_shared(ute); +} + +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); + entries.insert(std::make_shared(ute)); + valuesToEntries[doubleValue] = std::make_shared(ute); +} + +bool UnionTable::isQubitAlwaysOne(const Value q) const { + const unsigned int qubitIndex = qubitsToGlobalIndices.at(q); + const auto ute = valuesToEntries.at(q); + for (auto const& hs : ute->states) { + if (!hs.isQubitAlwaysOne(qubitIndex)) { + return false; + } + } + return true; +} + +bool UnionTable::isQubitAlwaysZero(const Value q) const { + const unsigned int qubitIndex = qubitsToGlobalIndices.at(q); + const auto ute = valuesToEntries.at(q); + for (auto const& hs : ute->states) { + if (!hs.isQubitAlwaysZero(qubitIndex)) { + return false; + } + } + return true; +} + +bool UnionTable::isClassicalValueAlwaysTrue(const Value c) const { + const auto ute = valuesToEntries.at(c); + for (auto const& hs : ute->states) { + if (!hs.isValueTrue(c)) { + return false; + } + } + return true; +} + +bool UnionTable::isClassicalValueAlwaysFalse(const Value c) const { + const auto ute = valuesToEntries.at(c); + for (auto const& hs : ute->states) { + if (hs.isValueTrue(c)) { + return false; + } + } + return true; +} + +bool UnionTable::hasAlwaysZeroProbability( + const llvm::DenseMap& qubitValues, + const llvm::DenseMap& classicalValues) const { + std::set participatingEntries; + for (auto& [qV, _] : qubitValues) { + participatingEntries.insert(*valuesToEntries.at(qV)); + } + for (auto& [cV, _] : classicalValues) { + participatingEntries.insert(*valuesToEntries.at(cV)); + } + for (const auto& ute : participatingEntries) { + 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 std::optional(0.0); + } + if (!(isa(op) || isa(op) || isa(op) || isa(op) || + isa(op))) { + return std::optional(); + } + const auto targetIndex = qubitsToGlobalIndices.at(target); + const auto targetUte = valuesToEntries.at(target); + 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 std::optional(0.0); + } + if (!alwaysOne && ctrlsQuantum.empty() && posCtrlsClassical.empty() && + negCtrlsClassical.empty()) { + return std::optional(); + } + + 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) { + 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 std::optional(); + } + } + if (!highestStateReachable) { + return std::optional(0.0); + } + if (!highestStateAlwaysReached) { + return std::optional(); + } + + // Only highest state reachable, return respective phase + if (isa(op)) { + return std::optional(std::numbers::pi); + } + if (isa(op)) { + return std::optional(std::numbers::pi / 2); + } + if (isa(op)) { + return std::optional(3.0 * std::numbers::pi / 2); + } + if (isa(op)) { + return std::optional(std::numbers::pi / 4); + } + // Tdg Op + return std::optional(-std::numbers::pi / 2); +} + +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; + for (const auto& hs : valuesToEntries.at(qCtrl)->states) { + 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/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index fb52a87f25..1f81e10f8c 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -98,6 +98,11 @@ static llvm::cl::opt enableHadamardLifting( llvm::cl::desc("Apply Hadamard lifting during optimization"), llvm::cl::init(false)); +static cl::opt enableConstantPropagation( + "constant-propagation", + cl::desc("Apply constant propagation during optimization"), + cl::init(false)); + /** * @brief Load and parse a `.qasm` file */ @@ -213,6 +218,7 @@ int main(int argc, char** argv) { config.disableMergeSingleQubitRotationGates = disableMergeSingleQubitRotationGates; config.enableHadamardLifting = enableHadamardLifting; + config.enableConstantPropagation = enableConstantPropagation; // Run the compilation pipeline CompilationRecord record; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index ddc3e4ce4d..42d9c328ef 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -120,12 +120,14 @@ class CompilerPipelineTest static void runPipeline(const mlir::ModuleOp module, const bool convertToQIR, const bool disableMergeSingleQubitRotationGates, const bool enableHadamardLifting, + const bool enableConstantPropagation, mlir::CompilationRecord& record) { mlir::QuantumCompilerConfig config; config.convertToQIRAdaptive = convertToQIR; config.disableMergeSingleQubitRotationGates = disableMergeSingleQubitRotationGates; config.enableHadamardLifting = enableHadamardLifting; + config.enableConstantPropagation = enableConstantPropagation; config.recordIntermediates = true; config.printIRAfterAllStages = true; @@ -169,7 +171,7 @@ TEST_P(CompilerPipelineTest, EndToEndPipeline) { EXPECT_TRUE(mlir::verify(*module).succeeded()); mlir::CompilationRecord record; - runPipeline(module.get(), testCase.convertToQIR, false, false, record); + runPipeline(module.get(), testCase.convertToQIR, false, false, false, record); ASSERT_TRUE(testCase.qcReferenceBuilder); auto qcReference = buildQCReference(testCase.qcReferenceBuilder); @@ -216,7 +218,7 @@ TEST_F(CompilerPipelineTest, RotationGateMergingPass) { ASSERT_TRUE(module); mlir::CompilationRecord record; - runPipeline(module.get(), false, false, false, record); + runPipeline(module.get(), false, false, false, false, record); // The outputs must differ, proving the pass ran and transformed the IR EXPECT_NE(record.afterQCOCanon, record.afterOptimization); @@ -239,7 +241,32 @@ TEST_F(CompilerPipelineTest, HadamardLiftingPass) { ASSERT_TRUE(module); mlir::CompilationRecord record; - runPipeline(module.get(), false, true, true, record); + runPipeline(module.get(), false, true, true, false, record); + + // The outputs must differ, proving the pass ran and transformed the IR + EXPECT_NE(record.afterQCOCanon, record.afterOptimization); +} + +/** + * @brief Test: Constant propagation pass is invoked during the optimization + * stage + * + * We run the pipeline with enabled constant propagation and check whether the + * outputs differ, i.e. that the pipeline ran and changed the IR. + * Correctness of the pass is tested in a dedicated test. + */ +TEST_F(CompilerPipelineTest, ConstantPropagationPass) { + auto module = mlir::qc::QCProgramBuilder::build( + context.get(), [&](mlir::qc::QCProgramBuilder& b) { + auto q0 = b.allocQubit(); + auto q1 = b.allocQubit(); + b.x(q0); + b.cx(q0, q1); + }); + ASSERT_TRUE(module); + + mlir::CompilationRecord record; + runPipeline(module.get(), false, true, false, true, record); // The outputs must differ, proving the pass ran and transformed the IR EXPECT_NE(record.afterQCOCanon, record.afterOptimization); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt index e80e957680..10ebaf0870 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt @@ -7,12 +7,20 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-optimizations) -add_executable(${target_name} test_qco_hadamard_lifting.cpp - test_qco_merge_single_qubit_rotation.cpp test_quantum_loop_unroll.cpp) +add_executable( + ${target_name} + ConstantPropagation/test_hybridState.cpp + ConstantPropagation/test_quantumState.cpp + ConstantPropagation/test_unionTable.cpp + test_qco_constant_propagation.cpp + test_qco_hadamard_lifting.cpp + test_qco_merge_single_qubit_rotation.cpp + test_quantum_loop_unroll.cpp) target_link_libraries( ${target_name} - PRIVATE GTest::gtest_main + PRIVATE GTest::gmock + GTest::gtest_main MLIRQCOProgramBuilder MLIRQCOTransforms MLIRQCOUtils 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..6d6f9e5b4a --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp @@ -0,0 +1,577 @@ +/* + * 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/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp" +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp" + +#include +#include +#include +#include + +#include +#include + +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]; + } + + void TearDown() override {} +}; + +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); + constexpr auto 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, handleErrorIfTwoManyAmplitudesAreNonzero) { + 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, doMeasurementWithNegClassicalCtrl) { + 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, doResetWithNegClassicalCtrl) { + 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); + hState1.addIntegerValue(v2, 7); + hState1.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); + + const auto hState2 = HybridState({}, 10, 0.5); + hState1.addIntegerValue(v2, 7); + hState1.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); + + const auto hState2 = HybridState({}, 10, 0.5); + hState1.addIntegerValue(v2, 7); + hState1.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(), val2.getType(), val1); + + 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"))); +} 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..cd672da510 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_quantumState.cpp @@ -0,0 +1,342 @@ +/* + * 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/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +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}; + + HOp hOp; + XOp xOp; + ZOp zOp; + SOp sOp; + TdgOp tdgOp; + UOp uOp; + DCXOp dcxOp; + SWAPOp swapOp; + + 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); + 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]}); + 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]); + } + + void TearDown() override {} +}; + +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, propagateGateCheckErrorIfTwoManyAmplitudesAreNonzero) { + auto qState = QuantumState(fourQubits, 2); + qState.propagateGate(hOp.getOperation(), vectorThree); + qState.propagateGate(xOp.getOperation(), vectorTwo, vectorThree); + + EXPECT_THROW(qState.propagateGate(hOp.getOperation(), vectorTwo); + , std::domain_error); +} + +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_THROW(auto qs = qState1.unify(qState2), std::domain_error); +} 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..2c1eceb289 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_unionTable.cpp @@ -0,0 +1,1186 @@ +/* + * 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 + +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; + + 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]); + 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); + } + + void TearDown() override {} +}; + +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, 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; + + 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(7); + 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); + } + + void TearDown() override {} +}; + +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, 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}")); +} + +class UnionTablePropertiesTest : public testing::Test { +protected: + mlir::MLIRContext context; + QCOProgramBuilder programBuilder; + UnionTable ut = UnionTable(3, 2); + + HOp hOp; + XOp xOp; + ZOp zOp; + 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]); + 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); + } + + void TearDown() override {} +}; + +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, 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; + + 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; + + SmallUnionTableTest() : programBuilder(&context) {} + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + programBuilder.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]; + v7 = q[7]; + v8 = q[8]; + + q0 = {v0}; + q1 = {v1}; + q2 = {v2}; + q3 = {v3}; + q4 = {v4}; + q5 = {v5}; + q6 = {v6}; + q7 = {v7}; + q8 = {v8}; + + 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); + + EXPECT_THAT(ut.toString(), + testing::HasSubstr("Qubits: 321, HybridStates: {TOP}")); +} + +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 + } + + void TearDown() override {} +}; + +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); +} 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..b9241358ee --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_constant_propagation.cpp @@ -0,0 +1,674 @@ +/* + * 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 + +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 hadamardLiftingPass to the current context and runs it. + */ + static LogicalResult runConstantPropagationPass(ModuleOp 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 target is not + * in |0> or |1>. + */ +TEST_F(QCOConstantPropagationTest, testDontRemoveIfTargetInSuperposition) { + 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 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(3); + 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); + auto qRange01 = programBuilder.qcoIf( + b0, {q01, q1}, + [&](const ValueRange args) { return SmallVector{args[0], args[1]}; }, + [&](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(3); + 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 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 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 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 quantum conditional is replaced by a + * classical if a qubit and a classical bit are equivalent. + */ +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())); +}