diff --git a/.clang-tidy b/.clang-tidy index 8683b18a0..65f2796cd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -74,3 +74,5 @@ CheckOptions: value: CamelCase - key: readability-identifier-naming.VariableCase value: camelBack + - key: misc-include-cleaner.IgnoreHeaders + value: "Eigen/.*;unsupported/Eigen/.*" diff --git a/CMakeLists.txt b/CMakeLists.txt index 86f93ccc8..0f6473a6d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,18 @@ include(PreventInSourceBuilds) include(PackageAddTest) include(Cache) include(AddMQTCoreLibrary) +include(FetchContent) + +FetchContent_Declare( + Eigen + GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git + GIT_TAG 3.4.1 + GIT_SHALLOW TRUE) +FetchContent_MakeAvailable(Eigen) +if(WIN32 AND ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL ARM64) + message(STATUS "Enabling non-optimal vectorization in Eigen to avoid alignment issues") + add_compile_definitions(EIGEN_DONT_ALIGN_STATICALLY) +endif() option(BUILD_MQT_CORE_BINDINGS "Build the MQT Core Python bindings" OFF) if(BUILD_MQT_CORE_BINDINGS) diff --git a/mlir/include/mlir/CMakeLists.txt b/mlir/include/mlir/CMakeLists.txt index 9add328fe..35666595c 100644 --- a/mlir/include/mlir/CMakeLists.txt +++ b/mlir/include/mlir/CMakeLists.txt @@ -6,5 +6,6 @@ # # Licensed under the MIT License -add_subdirectory(Dialect) add_subdirectory(Conversion) +add_subdirectory(Dialect) +add_subdirectory(Passes) diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index 8e8ae639d..7c097e6ea 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -119,6 +119,11 @@ class QuantumCompilerPipeline { */ static void addCleanupPasses(PassManager& pm); + /** + * @brief Add all available optimization passes + */ + static void addOptimizationPasses(PassManager& pm); + /** * @brief Configure PassManager with diagnostic options * diff --git a/mlir/include/mlir/Dialect/QCO/IR/CMakeLists.txt b/mlir/include/mlir/Dialect/QCO/IR/CMakeLists.txt index bddaba3d7..fd4b0372d 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/CMakeLists.txt +++ b/mlir/include/mlir/Dialect/QCO/IR/CMakeLists.txt @@ -7,6 +7,6 @@ # Licensed under the MIT License add_mlir_dialect(QCOOps qco) -add_mlir_interface(QCOInterfaces) +add_mlir_interface(QCOInterfaces LINK_LIBS PUBLIC Eigen3::Eigen) add_mlir_doc(QCOOps MLIRQCODialect Dialects/ -gen-dialect-doc) add_mlir_doc(QCOInterfaces MLIRQCOInterfaces Dialects/ -gen-op-interface-docs -dialect=qco) diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h b/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h index d1087ab6f..4098090a5 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h +++ b/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h @@ -21,6 +21,10 @@ #pragma clang diagnostic pop #endif +#include "mlir/Dialect/Utils/MatrixUtils.h" + +#include +#include #include #include #include @@ -28,6 +32,7 @@ #include #include #include +#include #include #define DIALECT_NAME_QCO "qco" @@ -43,6 +48,7 @@ //===----------------------------------------------------------------------===// #define GET_TYPEDEF_CLASSES +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h.inc" #include "mlir/Dialect/QCO/IR/QCOOpsTypes.h.inc" //===----------------------------------------------------------------------===// @@ -51,6 +57,13 @@ namespace mlir::qco { +/** + * @brief Retrieve C++ type of static mlir::Value. + * @details The returned float attribute can be used to get the value of the + * given parameter as a C++ type. + */ +[[nodiscard]] inline std::optional +tryGetParameterAsDouble(UnitaryOpInterface op, size_t i); /** * @brief Trait for operations with a fixed number of target qubits and * parameters @@ -60,8 +73,15 @@ namespace mlir::qco { * verification and code generation optimizations. * @tparam T The target arity. * @tparam P The parameter arity. + * @tparam MatrixDefinition A function returning the matrix definition of the + * operation. The operation will be provided as the + * only argument of the function. If the operation does + * not have a matrix definition, set this value to + * nullptr. */ -template class TargetAndParameterArityTrait { +template +class TargetAndParameterArityTrait { public: template class Impl : public OpTrait::TraitBase { @@ -108,6 +128,11 @@ template class TargetAndParameterArityTrait { return this->getOperation()->getOperand(T + i); } + /** + * @brief Retrieve mlir::FloatAttr of static mlir::Value. + * @details The returned float attribute can be used to get the value of the + * given parameter as a C++ type or perform other mlir operations. + */ [[nodiscard]] static FloatAttr getStaticParameter(Value param) { auto constantOp = param.getDefiningOp(); if (!constantOp) { @@ -116,6 +141,13 @@ template class TargetAndParameterArityTrait { return dyn_cast(constantOp.getValue()); } + [[nodiscard]] UnitaryMatrixType getUnitaryMatrixDefinition() const + requires(MatrixDefinition != nullptr) + { + const auto* op = this->getConstOperation(); + return MatrixDefinition(llvm::dyn_cast(op)); + } + Value getInputForOutput(Value output) { const auto& op = this->getOperation(); for (size_t i = 0; i < T; ++i) { @@ -136,12 +168,93 @@ template class TargetAndParameterArityTrait { llvm::reportFatalUsageError( "Given qubit is not an input of the operation"); } + + protected: + [[nodiscard]] const Operation* getConstOperation() const { + auto* concrete = static_cast(this); + // use dereference operator instead of getOperation() of mlir::Op; the + // operator provides a const overload, getOperation() does not + return *concrete; + } }; }; } // namespace mlir::qco -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h.inc" // IWYU pragma: export +// #include "mlir/Dialect/QCO/IR/QCOInterfaces.h.inc" // IWYU pragma: export + +//===----------------------------------------------------------------------===// +// Operations Helpers +//===----------------------------------------------------------------------===// + +namespace mlir::qco { + +[[nodiscard]] inline std::optional +tryGetParameterAsDouble(UnitaryOpInterface op, size_t i) { + using DummyArityType = + TargetAndParameterArityTrait<0, 0, Eigen::MatrixXcd, nullptr>; + const auto param = op.getParameter(i); + const auto floatAttr = + DummyArityType::Impl::getStaticParameter(param); + if (!floatAttr) { + return std::nullopt; + } + return floatAttr.getValueAsDouble(); +} + +[[nodiscard]] inline std::pair +permutate(const Eigen::MatrixXcd& inputMatrix, + const Eigen::VectorXi& permutation) { + const auto swapMatrix = utils::getMatrixSWAP(); + + auto dim = inputMatrix.cols(); + assert(inputMatrix.cols() == inputMatrix.rows()); + assert(dim == permutation.size()); + + Eigen::MatrixXcd permutatedMatrix(dim, dim); + Eigen::VectorXi undoPermutation(permutation.size()); + for (int i = 0; i < permutation.size(); ++i) { + undoPermutation(permutation(i)) = i; + // TODO + } + + return {permutatedMatrix, undoPermutation}; +} + +[[nodiscard]] inline Eigen::MatrixXcd getBlockMatrix(size_t dim, + mlir::Region& region) { + // TODO: check if dim == region.getArguments().size() + assert(dim == 1); // TODO: remove once permutations are properly handled + + Eigen::MatrixXcd result = Eigen::MatrixXcd::Identity(1 << dim, 1 << dim); + for (auto&& block : region) { + for (auto&& op : block) { + auto unitaryOp = llvm::dyn_cast(op); + if (unitaryOp) { + return result; + } + auto matrix = unitaryOp.getUnitaryMatrix(); + size_t matrixDim = matrix.cols(); + if (matrixDim < dim) { + // TODO: permutate such that operation qubits are next to each other; + // then perform front/back padding accordingly + + auto paddingDim = dim - matrixDim; + auto padding = + Eigen::MatrixXcd::Identity(1 << paddingDim, 1 << paddingDim); + matrix = Eigen::kroneckerProduct(matrix, padding); + + // TODO: undo permutation + } + result = matrix * result; + } + } + return result; +} + +mlir::Region& getCtrlBody(UnitaryOpInterface op); + +} // namespace mlir::qco //===----------------------------------------------------------------------===// // Operations @@ -149,3 +262,11 @@ template class TargetAndParameterArityTrait { #define GET_OP_CLASSES #include "mlir/Dialect/QCO/IR/QCOOps.h.inc" // IWYU pragma: export + +namespace mlir::qco { + +[[nodiscard]] inline mlir::Region& getCtrlBody(UnitaryOpInterface op) { + return llvm::cast(op).getBody(); +} + +} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td index 8e71a52a5..97b3776af 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td @@ -28,6 +28,7 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { let cppNamespace = "::mlir::qco"; + // TODO: fix const correctness? let methods = [ // Qubit accessors InterfaceMethod< @@ -89,17 +90,17 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { InterfaceMethod< "Returns true if the operation has any control qubits, otherwise false.", "bool", "isControlled", (ins), - [{ return getNumControls(impl, tablegen_opaque_val) > 0; }] + [{ return $_op.getNumControls() > 0; }] >, InterfaceMethod< "Returns true if the operation only acts on a single qubit.", "bool", "isSingleQubit", (ins), - [{ return getNumQubits(impl, tablegen_opaque_val) == 1; }] + [{ return $_op.getNumQubits() == 1; }] >, InterfaceMethod< "Returns true if the operation acts on two qubits.", "bool", "isTwoQubit", (ins), - [{ return getNumQubits(impl, tablegen_opaque_val) == 2; }] + [{ return $_op.getNumQubits() == 2; }] >, // Identification @@ -107,7 +108,27 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { "Returns the base symbol/mnemonic of the operation.", "StringRef", "getBaseSymbol", (ins) >, + + // Unitary matrix methods + InterfaceMethod< + "Returns the unitary matrix definition of the operation.", + "Eigen::MatrixXcd", "getUnitaryMatrix", (ins), + [{ + if constexpr (requires { $_op.getUnitaryMatrixDefinition(); }) { + return $_op.getUnitaryMatrixDefinition(); + } else { + llvm::reportFatalUsageError("Operation '" + $_op.getBaseSymbol() + "' has no unitary matrix definition!"); + } + }] + >, ]; + + let extraTraitClassDeclaration = [{ + template + MatrixType getFastUnitaryMatrix() const { + return $_op.getUnitaryMatrixDefinition(); + } + }]; } #endif // QCO_INTERFACES diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td index 1dea491ab..2086d6aba 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td @@ -238,11 +238,18 @@ def ResetOp : QCOOp<"reset", [Idempotent, SameOperandsAndResultType]> { // Traits //===----------------------------------------------------------------------===// -class TargetAndParameterArityTrait - : ParamNativeOpTrait<"TargetAndParameterArityTrait", !strconcat(!cast(T), ",", !cast(P))> { +class MatrixDefinitionLambda { + code MatrixType = "Eigen::Matrix, " # MatrixSize # ", " # MatrixSize # ">"; + code Code = !cond(!empty(MatrixDefinitionBody): "nullptr", true: "[](UnitaryOpInterface op) -> " # MatrixType # " { " # MatrixDefinitionBody # " }"); +} +class FixedSizeMatrixDefinitionLambda : MatrixDefinitionLambda<"1 << " # !cast(T), MatrixDefinitionBody>; + +class TargetAndParameterArityTrait> + : ParamNativeOpTrait<"TargetAndParameterArityTrait", !cast(T) # ", " # !cast(P) # ", " # MatrixDefinition.MatrixType # ", " # MatrixDefinition.Code> { let cppNamespace = "::mlir::qco"; } +def ZeroTargetZeroParameter : TargetAndParameterArityTrait<0, 0>; def ZeroTargetOneParameter : TargetAndParameterArityTrait<0, 1>; def OneTargetZeroParameter : TargetAndParameterArityTrait<1, 0>; def OneTargetOneParameter : TargetAndParameterArityTrait<1, 1>; @@ -252,11 +259,22 @@ def TwoTargetZeroParameter : TargetAndParameterArityTrait<2, 0>; def TwoTargetOneParameter : TargetAndParameterArityTrait<2, 1>; def TwoTargetTwoParameter : TargetAndParameterArityTrait<2, 2>; +class DynamicTargetZeroParameterUnitaryMatrix : TargetAndParameterArityTrait<0, 0, MatrixDefinitionLambda<"Eigen::Dynamic", MatrixDefinitionBody>>; + +class ZeroTargetOneParameterUnitaryMatrix : TargetAndParameterArityTrait<0, 1, FixedSizeMatrixDefinitionLambda<0, MatrixDefinitionBody>>; +class OneTargetZeroParameterUnitaryMatrix : TargetAndParameterArityTrait<1, 0, FixedSizeMatrixDefinitionLambda<1, MatrixDefinitionBody>>; +class OneTargetOneParameterUnitaryMatrix : TargetAndParameterArityTrait<1, 1, FixedSizeMatrixDefinitionLambda<1, MatrixDefinitionBody>>; +class OneTargetTwoParameterUnitaryMatrix : TargetAndParameterArityTrait<1, 2, FixedSizeMatrixDefinitionLambda<1, MatrixDefinitionBody>>; +class OneTargetThreeParameterUnitaryMatrix : TargetAndParameterArityTrait<1, 3, FixedSizeMatrixDefinitionLambda<1, MatrixDefinitionBody>>; +class TwoTargetZeroParameterUnitaryMatrix : TargetAndParameterArityTrait<2, 0, FixedSizeMatrixDefinitionLambda<2, MatrixDefinitionBody>>; +class TwoTargetOneParameterUnitaryMatrix : TargetAndParameterArityTrait<2, 1, FixedSizeMatrixDefinitionLambda<2, MatrixDefinitionBody>>; +class TwoTargetTwoParameterUnitaryMatrix : TargetAndParameterArityTrait<2, 2, FixedSizeMatrixDefinitionLambda<2, MatrixDefinitionBody>>; + //===----------------------------------------------------------------------===// // Unitary Operations //===----------------------------------------------------------------------===// -def GPhaseOp : QCOOp<"gphase", traits = [UnitaryOpInterface, ZeroTargetOneParameter, MemoryEffects<[MemWrite]>]> { +def GPhaseOp : QCOOp<"gphase", traits = [UnitaryOpInterface, ZeroTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixGPhase(tryGetParameterAsDouble(op, 0).value()); }]>, MemoryEffects<[MemWrite]>]> { let summary = "Apply a global phase to the state"; let description = [{ Applies a global phase to the state. @@ -281,7 +299,7 @@ def GPhaseOp : QCOOp<"gphase", traits = [UnitaryOpInterface, ZeroTargetOneParame let hasCanonicalizer = 1; } -def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixId(); }]>]> { let summary = "Apply an Id gate to a qubit"; let description = [{ Applies an Id gate to a qubit and returns the transformed qubit. @@ -303,7 +321,7 @@ def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def XOp : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def XOp : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixX(); }]>]> { let summary = "Apply an X gate to a qubit"; let description = [{ Applies an X gate to a qubit and returns the transformed qubit. @@ -325,7 +343,7 @@ def XOp : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def YOp : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def YOp : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixY(); }]>]> { let summary = "Apply a Y gate to a qubit"; let description = [{ Applies a Y gate to a qubit and returns the transformed qubit. @@ -347,7 +365,7 @@ def YOp : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixZ(); }]>]> { let summary = "Apply a Z gate to a qubit"; let description = [{ Applies a Z gate to a qubit and returns the transformed qubit. @@ -369,7 +387,7 @@ def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def HOp : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def HOp : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixH(); }]>]> { let summary = "Apply a H gate to a qubit"; let description = [{ Applies a H gate to a qubit and returns the transformed qubit. @@ -391,7 +409,7 @@ def HOp : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def SOp : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SOp : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixS(); }]>]> { let summary = "Apply an S gate to a qubit"; let description = [{ Applies an S gate to a qubit and returns the transformed qubit. @@ -413,7 +431,7 @@ def SOp : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def SdgOp : QCOOp<"sdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SdgOp : QCOOp<"sdg", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixSdg(); }]>]> { let summary = "Apply an Sdg gate to a qubit"; let description = [{ Applies an Sdg gate to a qubit and returns the transformed qubit. @@ -435,7 +453,7 @@ def SdgOp : QCOOp<"sdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> let hasCanonicalizer = 1; } -def TOp : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def TOp : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixT(); }]>]> { let summary = "Apply a T gate to a qubit"; let description = [{ Applies a T gate to a qubit and returns the transformed qubit. @@ -457,7 +475,7 @@ def TOp : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def TdgOp : QCOOp<"tdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def TdgOp : QCOOp<"tdg", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixTdg(); }]>]> { let summary = "Apply a Tdg gate to a qubit"; let description = [{ Applies a Tdg gate to a qubit and returns the transformed qubit. @@ -479,7 +497,7 @@ def TdgOp : QCOOp<"tdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> let hasCanonicalizer = 1; } -def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixSX(); }]>]> { let summary = "Apply an SX gate to a qubit"; let description = [{ Applies an SX gate to a qubit and returns the transformed qubit. @@ -501,7 +519,7 @@ def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def SXdgOp : QCOOp<"sxdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SXdgOp : QCOOp<"sxdg", traits = [UnitaryOpInterface, OneTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixSXdg(); }]>]> { let summary = "Apply an SXdg gate to a qubit"; let description = [{ Applies an SXdg gate to a qubit and returns the transformed qubit. @@ -523,7 +541,7 @@ def SXdgOp : QCOOp<"sxdg", traits = [UnitaryOpInterface, OneTargetZeroParameter] let hasCanonicalizer = 1; } -def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixRX(tryGetParameterAsDouble(op, 0).value()); }]>]> { let summary = "Apply an RX gate to a qubit"; let description = [{ Applies an RX gate to a qubit and returns the transformed qubit. @@ -550,7 +568,7 @@ def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixRY(tryGetParameterAsDouble(op, 0).value()); }]>]> { let summary = "Apply an RY gate to a qubit"; let description = [{ Applies an RY gate to a qubit and returns the transformed qubit. @@ -577,7 +595,7 @@ def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixRZ(tryGetParameterAsDouble(op, 0).value()); }]>]> { let summary = "Apply an RZ gate to a qubit"; let description = [{ Applies an RZ gate to a qubit and returns the transformed qubit. @@ -604,7 +622,7 @@ def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def POp : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def POp : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixP(tryGetParameterAsDouble(op, 0).value()); }]>]> { let summary = "Apply a P gate to a qubit"; let description = [{ Applies a P gate to a qubit and returns the transformed qubit. @@ -631,7 +649,7 @@ def POp : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def ROp : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { +def ROp : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameterUnitaryMatrix<[{ return utils::getMatrixR(tryGetParameterAsDouble(op, 0).value(), tryGetParameterAsDouble(op, 1).value()); }]>]> { let summary = "Apply an R gate to a qubit"; let description = [{ Applies an R gate to a qubit and returns the transformed qubit. @@ -659,7 +677,7 @@ def ROp : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { let hasCanonicalizer = 1; } -def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { +def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameterUnitaryMatrix<[{ return utils::getMatrixU2(tryGetParameterAsDouble(op, 0).value(), tryGetParameterAsDouble(op, 1).value()); }]>]> { let summary = "Apply a U2 gate to a qubit"; let description = [{ Applies a U2 gate to a qubit and returns the transformed qubit. @@ -687,7 +705,7 @@ def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { let hasCanonicalizer = 1; } -def UOp : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameter]> { +def UOp : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameterUnitaryMatrix<[{ return utils::getMatrixU(tryGetParameterAsDouble(op, 0).value(), tryGetParameterAsDouble(op, 1).value(), tryGetParameterAsDouble(op, 2).value()); }]>]> { let summary = "Apply a U gate to a qubit"; let description = [{ Applies a U gate to a qubit and returns the transformed qubit. @@ -716,7 +734,7 @@ def UOp : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameter]> { let hasCanonicalizer = 1; } -def SWAPOp : QCOOp<"swap", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def SWAPOp : QCOOp<"swap", traits = [UnitaryOpInterface, TwoTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixSWAP(); }]>]> { let summary = "Apply a SWAP gate to two qubits"; let description = [{ Applies a SWAP gate to two qubits and returns the transformed qubits. @@ -739,7 +757,7 @@ def SWAPOp : QCOOp<"swap", traits = [UnitaryOpInterface, TwoTargetZeroParameter] let hasCanonicalizer = 1; } -def iSWAPOp : QCOOp<"iswap", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def iSWAPOp : QCOOp<"iswap", traits = [UnitaryOpInterface, TwoTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixiSWAP(); }]>]> { let summary = "Apply a iSWAP gate to two qubits"; let description = [{ Applies a iSWAP gate to two qubits and returns the transformed qubits. @@ -760,7 +778,7 @@ def iSWAPOp : QCOOp<"iswap", traits = [UnitaryOpInterface, TwoTargetZeroParamete }]; } -def DCXOp : QCOOp<"dcx", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def DCXOp : QCOOp<"dcx", traits = [UnitaryOpInterface, TwoTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixDCX(); }]>]> { let summary = "Apply a DCX gate to two qubits"; let description = [{ Applies a DCX gate to two qubits and returns the transformed qubits. @@ -781,7 +799,7 @@ def DCXOp : QCOOp<"dcx", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> }]; } -def ECROp : QCOOp<"ecr", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def ECROp : QCOOp<"ecr", traits = [UnitaryOpInterface, TwoTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixECR(); }]>]> { let summary = "Apply an ECR gate to two qubits"; let description = [{ Applies an ECR gate to two qubits and returns the transformed qubits. @@ -804,7 +822,7 @@ def ECROp : QCOOp<"ecr", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> let hasCanonicalizer = 1; } -def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixRXX(tryGetParameterAsDouble(op, 0).value()); }]>]> { let summary = "Apply an RXX gate to two qubits"; let description = [{ Applies an RXX gate to two qubits and returns the transformed qubits. @@ -832,7 +850,7 @@ def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixRYY(tryGetParameterAsDouble(op, 0).value()); }]>]> { let summary = "Apply an RYY gate to two qubits"; let description = [{ Applies an RYY gate to two qubits and returns the transformed qubits. @@ -860,7 +878,7 @@ def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixRZX(tryGetParameterAsDouble(op, 0).value()); }]>]> { let summary = "Apply an RZX gate to two qubits"; let description = [{ Applies an RZX gate to two qubits and returns the transformed qubits. @@ -888,7 +906,7 @@ def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameterUnitaryMatrix<[{ return utils::getMatrixRZZ(tryGetParameterAsDouble(op, 0).value()); }]>]> { let summary = "Apply an RZZ gate to two qubits"; let description = [{ Applies an RZZ gate to two qubits and returns the transformed qubits. @@ -916,7 +934,7 @@ def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def XXPlusYYOp : QCOOp<"xx_plus_yy", traits = [UnitaryOpInterface, TwoTargetTwoParameter]> { +def XXPlusYYOp : QCOOp<"xx_plus_yy", traits = [UnitaryOpInterface, TwoTargetTwoParameterUnitaryMatrix<[{ return utils::getMatrixXXPlusYY(tryGetParameterAsDouble(op, 0).value(), tryGetParameterAsDouble(op, 1).value()); }]>]> { let summary = "Apply an XX+YY gate to two qubits"; let description = [{ Applies an XX+YY gate to two qubits and returns the transformed qubits. @@ -945,7 +963,7 @@ def XXPlusYYOp : QCOOp<"xx_plus_yy", traits = [UnitaryOpInterface, TwoTargetTwoP let hasCanonicalizer = 1; } -def XXMinusYYOp : QCOOp<"xx_minus_yy", traits = [UnitaryOpInterface, TwoTargetTwoParameter]> { +def XXMinusYYOp : QCOOp<"xx_minus_yy", traits = [UnitaryOpInterface, TwoTargetTwoParameterUnitaryMatrix<[{ return utils::getMatrixXXMinusYY(tryGetParameterAsDouble(op, 0).value(), tryGetParameterAsDouble(op, 1).value()); }]>]> { let summary = "Apply an XX-YY gate to two qubits"; let description = [{ Applies an XX-YY gate to two qubits and returns the transformed qubits. @@ -974,7 +992,7 @@ def XXMinusYYOp : QCOOp<"xx_minus_yy", traits = [UnitaryOpInterface, TwoTargetTw let hasCanonicalizer = 1; } -def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface]> { +def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface, ZeroTargetZeroParameter]> { let summary = "Apply a barrier gate to a set of qubits"; let description = [{ Applies a barrier gate to a set of qubits and returns the transformed qubits. @@ -1039,6 +1057,7 @@ def YieldOp : QCOOp<"yield", traits = [Terminator]> { def CtrlOp : QCOOp<"ctrl", traits = [ UnitaryOpInterface, + DynamicTargetZeroParameterUnitaryMatrix<[{ return utils::getMatrixCtrl(op.getNumControls(), getBlockMatrix(op.getNumTargets(), getCtrlBody(op))); }]>, AttrSizedOperandSegments, AttrSizedResultSegments, SameOperandsAndResultType, diff --git a/mlir/include/mlir/Dialect/Utils/MatrixUtils.h b/mlir/include/mlir/Dialect/Utils/MatrixUtils.h new file mode 100644 index 000000000..16fa93e78 --- /dev/null +++ b/mlir/include/mlir/Dialect/Utils/MatrixUtils.h @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCODialect.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::utils { + +inline Eigen::Matrix, 1, 1> getMatrixGPhase(double theta) { + using namespace std::complex_literals; + return Eigen::Matrix, 1, 1>{std::exp(1i * theta)}; +} + +inline Eigen::Matrix2cd getMatrixId() { + using namespace std::complex_literals; + return Eigen::Matrix2cd{{1.0 + 0i, 0.0 + 0i}, // row 0 + {0.0 + 0i, 1.0 + 0i}}; // row 1 +} + +inline Eigen::Matrix2cd getMatrixX() { + using namespace std::complex_literals; + return Eigen::Matrix2cd{{0.0 + 0i, 1.0 + 0i}, // row 0 + {1.0 + 0i, 0.0 + 0i}}; // row 1 +} + +inline Eigen::Matrix2cd getMatrixY() { + using namespace std::complex_literals; + return Eigen::Matrix2cd{{0.0 + 0i, 0.0 - 1i}, // row 0 + {0.0 + 1i, 0.0 + 0i}}; // row 1 +} + +inline Eigen::Matrix2cd getMatrixZ() { + using namespace std::complex_literals; + return Eigen::Matrix2cd{{1.0 + 0i, 0.0 + 0i}, // row 0 + {0.0 + 0i, -1.0 + 0i}}; // row 1 +} + +inline Eigen::Matrix2cd getMatrixH() { + using namespace std::complex_literals; + const std::complex m00 = 1.0 / std::sqrt(2) + 0i; + const std::complex m11 = -1.0 / std::sqrt(2) + 0i; + return Eigen::Matrix2cd{{m00, m00}, {m00, m11}}; +} + +inline Eigen::Matrix2cd getMatrixS() { + using namespace std::complex_literals; + return Eigen::Matrix2cd{{1.0 + 0i, 0.0 + 0i}, // row 0 + {0.0 + 0i, 0.0 + 1i}}; // row 1 +} + +inline Eigen::Matrix2cd getMatrixSdg() { + using namespace std::complex_literals; + return Eigen::Matrix2cd{{1.0 + 0i, 0.0 + 0i}, // row 0 + {0.0 + 0i, 0.0 - 1i}}; // row 1 +} + +inline Eigen::Matrix2cd getMatrixT() { + using namespace std::complex_literals; + const std::complex m00 = 1.0 + 0i; + const std::complex m01 = 0.0 + 0i; + const std::complex m11 = std::exp(1i * std::numbers::pi / 4.0); + return Eigen::Matrix2cd{{m00, m01}, {m01, m11}}; +} + +inline Eigen::Matrix2cd getMatrixTdg() { + using namespace std::complex_literals; + const std::complex m00 = 1.0 + 0i; + const std::complex m01 = 0.0 + 0i; + const std::complex m11 = std::exp(-1i * std::numbers::pi / 4.0); + return Eigen::Matrix2cd{{m00, m01}, {m01, m11}}; +} + +inline Eigen::Matrix2cd getMatrixSX() { + using namespace std::complex_literals; + const std::complex m00 = (1.0 + 1i) / 2.0; + const std::complex m01 = (1.0 - 1i) / 2.0; + return Eigen::Matrix2cd{{m00, m01}, {m01, m00}}; +} + +inline Eigen::Matrix2cd getMatrixSXdg() { + using namespace std::complex_literals; + const std::complex m00 = (1.0 - 1i) / 2.0; + const std::complex m01 = (1.0 + 1i) / 2.0; + return Eigen::Matrix2cd{{m00, m01}, {m01, m00}}; +} + +inline Eigen::Matrix2cd getMatrixRX(double theta) { + using namespace std::complex_literals; + const std::complex m00 = std::cos(theta / 2.0) + 0i; + const std::complex m01 = -1i * std::sin(theta / 2.0); + return Eigen::Matrix2cd{{m00, m01}, {m01, m00}}; +} + +inline Eigen::Matrix2cd getMatrixRY(double theta) { + using namespace std::complex_literals; + const std::complex m00 = std::cos(theta / 2.0) + 0i; + const std::complex m01 = -std::sin(theta / 2.0) + 0i; + return Eigen::Matrix2cd{{m00, m01}, {-m01, m00}}; +} + +inline Eigen::Matrix2cd getMatrixRZ(double theta) { + using namespace std::complex_literals; + const std::complex m00 = std::exp(-1i * theta / 2.0); + const std::complex m01 = 0.0 + 0i; + const std::complex m11 = std::exp(1i * theta / 2.0); + return Eigen::Matrix2cd{{m00, m01}, {m01, m11}}; +} + +inline Eigen::Matrix2cd getMatrixP(double theta) { + using namespace std::complex_literals; + const std::complex m00 = 1.0 + 0i; + const std::complex m01 = 0.0 + 0i; + const std::complex m11 = std::exp(1i * theta); + return Eigen::Matrix2cd{{m00, m01}, {m01, m11}}; +} + +inline Eigen::Matrix2cd getMatrixU2(double phi, double lambda) { + using namespace std::complex_literals; + const std::complex m00 = 1.0 / std::sqrt(2) + 0i; + const std::complex m01 = -std::exp(1i * lambda) / std::sqrt(2); + const std::complex m10 = std::exp(1i * phi) / std::sqrt(2); + const std::complex m11 = std::exp(1i * (phi + lambda)) / std::sqrt(2); + return Eigen::Matrix2cd{{m00, m01}, {m10, m11}}; +} + +inline Eigen::Matrix2cd getMatrixU(double theta, double phi, double lambda) { + using namespace std::complex_literals; + const std::complex m00 = std::cos(theta / 2.0) + 0i; + const std::complex m01 = + -std::exp(1i * lambda) * std::sin(theta / 2.0); + const std::complex m10 = std::exp(1i * phi) * std::sin(theta / 2.0); + const std::complex m11 = + std::exp(1i * (phi + lambda)) * std::cos(theta / 2.0); + return Eigen::Matrix2cd{{m00, m01}, {m10, m11}}; +} + +inline Eigen::Matrix2cd getMatrixR(double theta, double phi) { + using namespace std::complex_literals; + const std::complex m00 = std::cos(theta / 2.0) + 0i; + const std::complex m01 = + -1i * std::exp(-1i * phi) * std::sin(theta / 2.0); + const std::complex m10 = + -1i * std::exp(1i * phi) * std::sin(theta / 2.0); + const std::complex m11 = std::cos(theta / 2.0) + 0i; + return Eigen::Matrix2cd{{m00, m01}, {m10, m11}}; +} + +inline Eigen::Matrix4cd getMatrixSWAP() { + using namespace std::complex_literals; + return Eigen::Matrix4cd{{1.0 + 0i, 0.0 + 0i, 0.0 + 0i, 0.0 + 0i}, // row 0 + {0.0 + 0i, 0.0 + 0i, 1.0 + 0i, 0.0 + 0i}, // row 1 + {0.0 + 0i, 1.0 + 0i, 0.0 + 0i, 0.0 + 0i}, // row 2 + {0.0 + 0i, 0.0 + 0i, 0.0 + 0i, 1.0 + 0i}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixiSWAP() { + using namespace std::complex_literals; + return Eigen::Matrix4cd{{1.0 + 0i, 0.0 + 0i, 0.0 + 0i, 0.0 + 0i}, // row 0 + {0.0 + 0i, 0.0 + 0i, 0.0 + 1i, 0.0 + 0i}, // row 1 + {0.0 + 0i, 0.0 + 1i, 0.0 + 0i, 0.0 + 0i}, // row 2 + {0.0 + 0i, 0.0 + 0i, 0.0 + 0i, 1.0 + 0i}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixDCX() { + using namespace std::complex_literals; + return Eigen::Matrix4cd{{1.0 + 0i, 0.0 + 0i, 0.0 + 0i, 0.0 + 0i}, // row 0 + {0.0 + 0i, 0.0 + 0i, 1.0 + 0i, 0.0 + 0i}, // row 1 + {0.0 + 0i, 0.0 + 0i, 0.0 + 0i, 1.0 + 0i}, // row 2 + {0.0 + 0i, 1.0 + 0i, 0.0 + 0i, 0.0 + 0i}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixECR() { + using namespace std::complex_literals; + const std::complex m0 = 0.0 + 0i; + const std::complex m1 = 1.0 / std::sqrt(2) + 0i; + const std::complex mi = 0.0 + 1i / std::sqrt(2); + return Eigen::Matrix4cd{{m0, m0, m1, mi}, // row 0 + {m0, m0, mi, m1}, // row 1 + {m1, -mi, m0, m0}, // row 2 + {-mi, m1, m0, m0}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixRXX(double theta) { + using namespace std::complex_literals; + const std::complex m0 = 0.0 + 0i; + const std::complex mc = std::cos(theta / 2.0) + 0i; + const std::complex ms = -1i * std::sin(theta / 2.0); + return Eigen::Matrix4cd{{mc, m0, m0, ms}, // row 0 + {m0, mc, ms, m0}, // row 1 + {m0, ms, mc, m0}, // row 2 + {ms, m0, m0, mc}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixRYY(double theta) { + using namespace std::complex_literals; + const std::complex m0 = 0.0 + 0i; + const std::complex mc = std::cos(theta / 2.0) + 0i; + const std::complex ms = 1i * std::sin(theta / 2.0); + return Eigen::Matrix4cd{{mc, m0, m0, ms}, // row 0 + {m0, mc, -ms, m0}, // row 1 + {m0, -ms, mc, m0}, // row 2 + {ms, m0, m0, mc}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixRZX(double theta) { + using namespace std::complex_literals; + const std::complex m0 = 0.0 + 0i; + const std::complex mc = std::cos(theta / 2.0) + 0i; + const std::complex ms = -1i * std::sin(theta / 2.0); + return Eigen::Matrix4cd{{mc, -ms, m0, m0}, // row 0 + {-ms, mc, m0, m0}, // row 1 + {m0, m0, mc, ms}, // row 2 + {m0, m0, ms, mc}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixRZZ(double theta) { + using namespace std::complex_literals; + const std::complex m0 = 0.0 + 0i; + const std::complex mp = std::exp(1i * theta / 2.0); + const std::complex mm = std::exp(-1i * theta / 2.0); + return Eigen::Matrix4cd{{mm, m0, m0, m0}, // row 0 + {m0, mp, m0, m0}, // row 1 + {m0, m0, mp, m0}, // row 2 + {m0, m0, m0, mm}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixXXPlusYY(double theta, double beta) { + using namespace std::complex_literals; + const std::complex m0 = 0.0 + 0i; + const std::complex m1 = 1.0 + 0i; + const std::complex mc = std::cos(theta / 2.0) + 0i; + const std::complex msp = + -1i * std::sin(theta / 2.0) * std::exp(1i * beta); + const std::complex msm = + -1i * std::sin(theta / 2.0) * std::exp(-1i * beta); + return Eigen::Matrix4cd{{m1, m0, m0, m0}, // row 0 + {m0, mc, msm, m0}, // row 1 + {m0, msp, mc, m0}, // row 2 + {m0, m0, m0, m1}}; // row 3 +} + +inline Eigen::Matrix4cd getMatrixXXMinusYY(double theta, double beta) { + using namespace std::complex_literals; + const std::complex m0 = 0.0 + 0i; + const std::complex m1 = 1.0 + 0i; + const std::complex mc = std::cos(theta / 2.0) + 0i; + const std::complex msp = + std::sin(theta / 2.0) * std::exp(-1i * beta) + 0i; + const std::complex msm = + -std::sin(theta / 2.0) * std::exp(1i * beta) + 0i; + return Eigen::Matrix4cd{{mc, m0, m0, msm}, // row 0 + {m0, m1, m0, m0}, // row 1 + {m0, m0, m1, m0}, // row 2 + {msp, m0, m0, mc}}; // row 3 +} + +inline Eigen::MatrixXcd getMatrixCtrl(size_t numControls, + Eigen::MatrixXcd targetMatrix) { + // Get dimensions of target matrix + const auto targetDim = targetMatrix.cols(); + assert(targetMatrix.cols() == targetMatrix.rows()); + + // Define dimensions and type of output matrix + const auto dim = static_cast((1 << numControls) * targetDim); + + // Allocate output matrix + Eigen::MatrixXcd matrix = Eigen::MatrixXcd::Identity(dim, dim); + + // TODO: apply permutation such that target qubits are last + + // Fill output matrix + for (int64_t i = 0; i < targetDim; ++i) { + for (int64_t j = 0; j < targetDim; ++j) { + matrix(dim - targetDim - j, dim - targetDim - i) = targetMatrix(j, i); + } + } + + // TODO: undo permutation + + return matrix; +} + +} // namespace mlir::utils diff --git a/mlir/include/mlir/Passes/CMakeLists.txt b/mlir/include/mlir/Passes/CMakeLists.txt new file mode 100644 index 000000000..03e903ad7 --- /dev/null +++ b/mlir/include/mlir/Passes/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls -name QCO) +add_public_tablegen_target(QcoPassesIncGen) +add_mlir_doc(Passes QcoPasses Passes/ -gen-pass-doc) diff --git a/mlir/include/mlir/Passes/Decomposition/BasisDecomposer.h b/mlir/include/mlir/Passes/Decomposition/BasisDecomposer.h new file mode 100644 index 000000000..cc4d510a0 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/BasisDecomposer.h @@ -0,0 +1,547 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "EulerBasis.h" +#include "EulerDecomposition.h" +#include "GateSequence.h" +#include "Helpers.h" +#include "UnitaryMatrices.h" +#include "WeylDecomposition.h" +#include "ir/Definitions.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +/** + * Decomposer that must be initialized with a two-qubit basis gate that will + * be used to generate a circuit equivalent to a canonical gate (RXX+RYY+RZZ). + */ +class TwoQubitBasisDecomposer { +public: + /** + * Create decomposer that allows two-qubit decompositions based on the + * specified basis gate. + * This basis gate will appear between 0 and 3 times in each decompositions. + * The order of qubits is relevant and will change the results accordingly. + * The decomposer cannot handle different basis gates in the same + * decomposition (different order of the qubits also counts as a different + * basis gate). + */ + static TwoQubitBasisDecomposer create(const Gate& basisGate, + fp basisFidelity) { + auto relativeEq = [](auto&& lhs, auto&& rhs, auto&& epsilon, + auto&& maxRelative) { + // Handle same infinities + if (lhs == rhs) { + return true; + } + + // Handle remaining infinities + if (std::isinf(lhs) || std::isinf(rhs)) { + return false; + } + + auto absDiff = std::abs(lhs - rhs); + + // For when the numbers are really close together + if (absDiff <= epsilon) { + return true; + } + + auto absLhs = std::abs(lhs); + auto absRhs = std::abs(rhs); + if (absRhs > absLhs) { + return absDiff <= absRhs * maxRelative; + } + return absDiff <= absLhs * maxRelative; + }; + const matrix2x2 k12RArr{ + {qfp(0., FRAC1_SQRT2), qfp(FRAC1_SQRT2, 0.)}, + {qfp(-FRAC1_SQRT2, 0.), qfp(0., -FRAC1_SQRT2)}, + }; + const matrix2x2 k12LArr{ + {qfp(0.5, 0.5), qfp(0.5, 0.5)}, + {qfp(-0.5, 0.5), qfp(0.5, -0.5)}, + }; + + const auto basisDecomposer = + decomposition::TwoQubitWeylDecomposition::create( + getTwoQubitMatrix(basisGate), basisFidelity); + const auto isSuperControlled = + relativeEq(basisDecomposer.a, qc::PI_4, 1e-13, 1e-09) && + relativeEq(basisDecomposer.c, 0.0, 1e-13, 1e-09); + + // Create some useful matrices U1, U2, U3 are equivalent to the basis, + // expand as Ui = Ki1.Ubasis.Ki2 + auto b = basisDecomposer.b; + auto temp = qfp(0.5, -0.5); + const matrix2x2 k11l{ + {temp * (M_IM * std::exp(qfp(0., -b))), temp * std::exp(qfp(0., -b))}, + {temp * (M_IM * std::exp(qfp(0., b))), temp * -std::exp(qfp(0., b))}}; + const matrix2x2 k11r{{FRAC1_SQRT2 * (IM * std::exp(qfp(0., -b))), + FRAC1_SQRT2 * -std::exp(qfp(0., -b))}, + {FRAC1_SQRT2 * std::exp(qfp(0., b)), + FRAC1_SQRT2 * (M_IM * std::exp(qfp(0., b)))}}; + const matrix2x2 k32lK21l{{FRAC1_SQRT2 * qfp(1., std::cos(2. * b)), + FRAC1_SQRT2 * (IM * std::sin(2. * b))}, + {FRAC1_SQRT2 * (IM * std::sin(2. * b)), + FRAC1_SQRT2 * qfp(1., -std::cos(2. * b))}}; + temp = qfp(0.5, 0.5); + const matrix2x2 k21r{ + {temp * (M_IM * std::exp(qfp(0., -2. * b))), + temp * std::exp(qfp(0., -2. * b))}, + {temp * (IM * std::exp(qfp(0., 2. * b))), + temp * std::exp(qfp(0., 2. * b))}, + }; + const matrix2x2 k22l{ + {qfp(FRAC1_SQRT2, 0.), qfp(-FRAC1_SQRT2, 0.)}, + {qfp(FRAC1_SQRT2, 0.), qfp(FRAC1_SQRT2, 0.)}, + }; + const matrix2x2 k22r{{C_ZERO, C_ONE}, {C_M_ONE, C_ZERO}}; + const matrix2x2 k31l{ + {FRAC1_SQRT2 * std::exp(qfp(0., -b)), + FRAC1_SQRT2 * std::exp(qfp(0., -b))}, + {FRAC1_SQRT2 * -std::exp(qfp(0., b)), + FRAC1_SQRT2 * std::exp(qfp(0., b))}, + }; + const matrix2x2 k31r{ + {IM * std::exp(qfp(0., b)), C_ZERO}, + {C_ZERO, M_IM * std::exp(qfp(0., -b))}, + }; + temp = qfp(0.5, 0.5); + const matrix2x2 k32r{ + {temp * std::exp(qfp(0., b)), temp * -std::exp(qfp(0., -b))}, + {temp * (M_IM * std::exp(qfp(0., b))), + temp * (M_IM * std::exp(qfp(0., -b)))}, + }; + auto k1lDagger = basisDecomposer.k1l.transpose().conjugate(); + auto k1rDagger = basisDecomposer.k1r.transpose().conjugate(); + auto k2lDagger = basisDecomposer.k2l.transpose().conjugate(); + auto k2rDagger = basisDecomposer.k2r.transpose().conjugate(); + // Pre-build the fixed parts of the matrices used in 3-part + // decomposition + auto u0l = k31l * k1lDagger; + auto u0r = k31r * k1rDagger; + auto u1l = k2lDagger * k32lK21l * k1lDagger; + auto u1ra = k2rDagger * k32r; + auto u1rb = k21r * k1rDagger; + auto u2la = k2lDagger * k22l; + auto u2lb = k11l * k1lDagger; + auto u2ra = k2rDagger * k22r; + auto u2rb = k11r * k1rDagger; + auto u3l = k2lDagger * k12LArr; + auto u3r = k2rDagger * k12RArr; + // Pre-build the fixed parts of the matrices used in the 2-part + // decomposition + auto q0l = k12LArr.transpose().conjugate() * k1lDagger; + auto q0r = k12RArr.transpose().conjugate() * IPZ * k1rDagger; + auto q1la = k2lDagger * k11l.transpose().conjugate(); + auto q1lb = k11l * k1lDagger; + auto q1ra = k2rDagger * IPZ * k11r.transpose().conjugate(); + auto q1rb = k11r * k1rDagger; + auto q2l = k2lDagger * k12LArr; + auto q2r = k2rDagger * k12RArr; + + return TwoQubitBasisDecomposer{ + basisGate, + basisFidelity, + basisDecomposer, + isSuperControlled, + u0l, + u0r, + u1l, + u1ra, + u1rb, + u2la, + u2lb, + u2ra, + u2rb, + u3l, + u3r, + q0l, + q0r, + q1la, + q1lb, + q1ra, + q1rb, + q2l, + q2r, + }; + } + + /** + * Perform decomposition using the basis gate of this decomposer. + * + * @param targetDecomposition Prepared Weyl decomposition of unitary matrix + * to be decomposed. + * @param target1qEulerBases List of euler bases that should be tried out to + * find the best one for each euler decomposition. + * All bases will be mixed to get the best overall + * result. + * @param basisFidelity Fidelity for lowering the number of basis gates + * required + * @param approximate If true, use basisFidelity or, if std::nullopt, use + * basisFidelity of this decomposer. If false, fidelity + * of 1.0 will be assumed. + * @param numBasisGateUses Force use of given number of basis gates. + */ + [[nodiscard]] std::optional twoQubitDecompose( + const decomposition::TwoQubitWeylDecomposition& targetDecomposition, + const llvm::SmallVector& target1qEulerBases, + std::optional basisFidelity, bool approximate, + std::optional numBasisGateUses) const { + auto getBasisFidelity = [&]() { + if (approximate) { + return basisFidelity.value_or(this->basisFidelity); + } + return static_cast(1.0); + }; + fp actualBasisFidelity = getBasisFidelity(); + auto traces = this->traces(targetDecomposition); + auto getDefaultNbasis = [&]() { + auto minValue = std::numeric_limits::min(); + auto minIndex = -1; + for (int i = 0; std::cmp_less(i, traces.size()); ++i) { + // lower fidelity means it becomes easier to choose a lower number of + // basis gates + auto value = helpers::traceToFidelity(traces[i]) * + std::pow(actualBasisFidelity, i); + if (value > minValue) { + minIndex = i; + minValue = value; + } + } + return minIndex; + }; + // number of basis gates that need to be used in the decomposition + auto bestNbasis = numBasisGateUses.value_or(getDefaultNbasis()); + auto chooseDecomposition = [&]() { + if (bestNbasis == 0) { + return decomp0(targetDecomposition); + } + if (bestNbasis == 1) { + return decomp1(targetDecomposition); + } + if (bestNbasis == 2) { + return decomp2Supercontrolled(targetDecomposition); + } + if (bestNbasis == 3) { + return decomp3Supercontrolled(targetDecomposition); + } + throw std::logic_error{"Invalid basis to use"}; + }; + auto decomposition = chooseDecomposition(); + llvm::SmallVector, 8> + eulerDecompositions; + for (auto&& decomp : decomposition) { + assert(helpers::isUnitaryMatrix(decomp)); + auto eulerDecomp = unitaryToGateSequenceInner(decomp, target1qEulerBases, + 0, true, std::nullopt); + eulerDecompositions.push_back(eulerDecomp); + } + TwoQubitGateSequence gates{ + .gates = {}, + .globalPhase = targetDecomposition.globalPhase, + }; + // Worst case length is 5x 1q gates for each 1q decomposition + 1x 2q + // gate We might overallocate a bit if the euler basis is different but + // the worst case is just 16 extra elements with just a String and 2 + // smallvecs each. This is only transient though as the circuit + // sequences aren't long lived and are just used to create a + // QuantumCircuit or DAGCircuit when we return to Python space. + constexpr auto twoQubitSequenceDefaultCapacity = 21; + gates.gates.reserve(twoQubitSequenceDefaultCapacity); + gates.globalPhase -= bestNbasis * basisDecomposer.globalPhase; + if (bestNbasis == 2) { + gates.globalPhase += qc::PI; + } + + auto addEulerDecomposition = [&](std::size_t index, QubitId qubitId) { + if (auto&& eulerDecomp = eulerDecompositions[index]) { + for (auto&& gate : eulerDecomp->gates) { + gates.gates.push_back({.type = gate.type, + .parameter = gate.parameter, + .qubitId = {qubitId}}); + } + gates.globalPhase += eulerDecomp->globalPhase; + } + }; + + for (std::size_t i = 0; i < bestNbasis; ++i) { + // add single-qubit decompositions before basis gate + addEulerDecomposition(2 * i, 0); + addEulerDecomposition((2 * i) + 1, 1); + + // add basis gate + gates.gates.push_back(basisGate); + } + + // add single-qubit decompositions after basis gate + addEulerDecomposition(2UL * bestNbasis, 0); + addEulerDecomposition((2UL * bestNbasis) + 1, 1); + + // large global phases can be generated by the decomposition, thus limit + // it to [0, +2*pi); TODO: can be removed, should be done by something + // like constant folding + gates.globalPhase = helpers::remEuclid(gates.globalPhase, qc::TAU); + + return gates; + } + +protected: + // NOLINTBEGIN(modernize-pass-by-value) + /** + * Constructs decomposer instance. + */ + TwoQubitBasisDecomposer( + Gate basisGate, fp basisFidelity, + const decomposition::TwoQubitWeylDecomposition& basisDecomposer, + bool isSuperControlled, const matrix2x2& u0l, const matrix2x2& u0r, + const matrix2x2& u1l, const matrix2x2& u1ra, const matrix2x2& u1rb, + const matrix2x2& u2la, const matrix2x2& u2lb, const matrix2x2& u2ra, + const matrix2x2& u2rb, const matrix2x2& u3l, const matrix2x2& u3r, + const matrix2x2& q0l, const matrix2x2& q0r, const matrix2x2& q1la, + const matrix2x2& q1lb, const matrix2x2& q1ra, const matrix2x2& q1rb, + const matrix2x2& q2l, const matrix2x2& q2r) + : basisGate{std::move(basisGate)}, basisFidelity{basisFidelity}, + basisDecomposer{basisDecomposer}, isSuperControlled{isSuperControlled}, + u0l{u0l}, u0r{u0r}, u1l{u1l}, u1ra{u1ra}, u1rb{u1rb}, u2la{u2la}, + u2lb{u2lb}, u2ra{u2ra}, u2rb{u2rb}, u3l{u3l}, u3r{u3r}, q0l{q0l}, + q0r{q0r}, q1la{q1la}, q1lb{q1lb}, q1ra{q1ra}, q1rb{q1rb}, q2l{q2l}, + q2r{q2r} {} + // NOLINTEND(modernize-pass-by-value) + + /** + * Calculate decompositions when no basis gate is required. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 0 uses of the + * basis gate. Result :math:`U_r` has trace: + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r\cdot U_\text{target}^{\dag})\Big\vert = + * 4\Big\vert (\cos(x)\cos(y)\cos(z)+ j \sin(x)\sin(y)\sin(z)\Big\vert + * + * which is optimal for all targets and bases + */ + [[nodiscard]] static llvm::SmallVector + decomp0(const decomposition::TwoQubitWeylDecomposition& target) { + return { + target.k1r * target.k2r, + target.k1l * target.k2l, + }; + } + + /** + * Calculate decompositions when one basis gate is required. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 1 use of the + * basis gate math:`\sim U_d(a, b, c)`. Result :math:`U_r` has trace: + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r \cdot U_\text{target}^{\dag})\Big\vert = + * 4\Big\vert \cos(x-a)\cos(y-b)\cos(z-c) + j + * \sin(x-a)\sin(y-b)\sin(z-c)\Big\vert + * + * which is optimal for all targets and bases with ``z==0`` or ``c==0``. + */ + [[nodiscard]] llvm::SmallVector + decomp1(const decomposition::TwoQubitWeylDecomposition& target) const { + // may not work for z != 0 and c != 0 (not always in Weyl chamber) + return { + basisDecomposer.k2r.transpose().conjugate() * target.k2r, + basisDecomposer.k2l.transpose().conjugate() * target.k2l, + target.k1r * basisDecomposer.k1r.transpose().conjugate(), + target.k1l * basisDecomposer.k1l.transpose().conjugate(), + }; + } + + /** + * Calculate decompositions when two basis gates are required. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 2 uses of the + * basis gate. + * + * For supercontrolled basis :math:`\sim U_d(\pi/4, b, 0)`, all b, result + * :math:`U_r` has trace + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r \cdot U_\text{target}^\dag) \Big\vert = + * 4\cos(z) + * + * which is the optimal approximation for basis of CNOT-class + * :math:`\sim U_d(\pi/4, 0, 0)` or DCNOT-class + * :math:`\sim U_d(\pi/4, \pi/4, 0)` and any target. It may be sub-optimal + * for :math:`b \neq 0` (i.e. there exists an exact decomposition for any + * target using :math:`B \sim U_d(\pi/4, \pi/8, 0)`, but it may not be this + * decomposition). This is an exact decomposition for supercontrolled basis + * and target :math:`\sim U_d(x, y, 0)`. No guarantees for + * non-supercontrolled basis. + */ + [[nodiscard]] llvm::SmallVector decomp2Supercontrolled( + const decomposition::TwoQubitWeylDecomposition& target) const { + if (!isSuperControlled) { + // TODO: make fatal error? check in constructor? + llvm::errs() + << "Basis gate of TwoQubitBasisDecomposer is not super-controlled " + "- no guarantee for exact decomposition with two basis gates\n"; + } + return { + q2r * target.k2r, + q2l * target.k2l, + q1ra * rzMatrix(2. * target.b) * q1rb, + q1la * rzMatrix(-2. * target.a) * q1lb, + target.k1r * q0r, + target.k1l * q0l, + }; + } + + /** + * Calculate decompositions when three basis gates are required. + * + * Decompose target with 3 uses of the basis. + * + * This is an exact decomposition for supercontrolled basis + * :math:`\sim U_d(\pi/4, b, 0)`, all b, and any target. No guarantees for + * non-supercontrolled basis. + */ + [[nodiscard]] llvm::SmallVector decomp3Supercontrolled( + const decomposition::TwoQubitWeylDecomposition& target) const { + if (!isSuperControlled) { + llvm::errs() + << "Basis gate of TwoQubitBasisDecomposer is not super-controlled " + "- no guarantee for exact decomposition with " + "three basis gates\n"; + } + return { + u3r * target.k2r, + u3l * target.k2l, + u2ra * rzMatrix(2. * target.b) * u2rb, + u2la * rzMatrix(-2. * target.a) * u2lb, + u1ra * rzMatrix(-2. * target.c) * u1rb, + u1l, + target.k1r * u0r, + target.k1l * u0l, + }; + } + + /** + * Calculate traces for a combination of the parameters of the canonical + * gates of the target and basis decompositions. + * This can be used to determine the smallest number of basis gates that are + * necessary to construct an equivalent to the canonical gate. + */ + [[nodiscard]] std::array + traces(const decomposition::TwoQubitWeylDecomposition& target) const { + return { + static_cast(4.) * + qfp(std::cos(target.a) * std::cos(target.b) * std::cos(target.c), + std::sin(target.a) * std::sin(target.b) * std::sin(target.c)), + static_cast(4.) * + qfp(std::cos(qc::PI_4 - target.a) * + std::cos(basisDecomposer.b - target.b) * std::cos(target.c), + std::sin(qc::PI_4 - target.a) * + std::sin(basisDecomposer.b - target.b) * + std::sin(target.c)), + qfp(4. * std::cos(target.c), 0.), + qfp(4., 0.), + }; + } + + /** + * Decompose a single-qubit unitary matrix into a single-qubit gate + * sequence. Multiple euler bases may be specified and the one with the + * least complexity will be chosen. + */ + [[nodiscard]] static OneQubitGateSequence unitaryToGateSequenceInner( + const matrix2x2& unitaryMat, + const llvm::SmallVector& targetBasisList, QubitId /*qubit*/, + // TODO: add error map here: per qubit a mapping of operation to error + // value for better calculateError() + bool simplify, std::optional atol) { + auto calculateError = [](const OneQubitGateSequence& sequence) -> fp { + return static_cast(sequence.complexity()); + }; + + auto minError = std::numeric_limits::max(); + OneQubitGateSequence bestCircuit; + for (auto targetBasis : targetBasisList) { + auto circuit = EulerDecomposition::generateCircuit( + targetBasis, unitaryMat, simplify, atol); + assert(circuit.getUnitaryMatrix().isApprox( + helpers::kroneckerProduct(IDENTITY_GATE, unitaryMat), + SANITY_CHECK_PRECISION)); + auto error = calculateError(circuit); + if (error < minError) { + bestCircuit = circuit; + minError = error; + } + } + return bestCircuit; + } + +private: + // basis gate of this decomposer instance + Gate basisGate{}; + // fidelity with which the basis gate decomposition has been calculated + fp basisFidelity; + // cached decomposition for basis gate + decomposition::TwoQubitWeylDecomposition basisDecomposer; + // true if basis gate is super-controlled + bool isSuperControlled; + + // pre-built components for decomposition with 3 basis gates + matrix2x2 u0l; + matrix2x2 u0r; + matrix2x2 u1l; + matrix2x2 u1ra; + matrix2x2 u1rb; + matrix2x2 u2la; + matrix2x2 u2lb; + matrix2x2 u2ra; + matrix2x2 u2rb; + matrix2x2 u3l; + matrix2x2 u3r; + + // pre-built components for decomposition with 2 basis gates + matrix2x2 q0l; + matrix2x2 q0r; + matrix2x2 q1la; + matrix2x2 q1lb; + matrix2x2 q1ra; + matrix2x2 q1rb; + matrix2x2 q2l; + matrix2x2 q2r; +}; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/EulerBasis.h b/mlir/include/mlir/Passes/Decomposition/EulerBasis.h new file mode 100644 index 000000000..887db1a27 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/EulerBasis.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include + +namespace mlir::qco::decomposition { +/** + * Largest number that will be assumed as zero for the euler decompositions. + */ +static constexpr auto DEFAULT_ATOL = 1e-12; + +/** + * EulerBasis for a euler decomposition. + * + * @note only the following bases are supported for now: ZYZ, ZXZ and XZX + */ +enum class EulerBasis : std::uint8_t { + U3 = 0, + U321 = 1, + U = 2, + PSX = 3, + U1X = 4, + RR = 5, + ZYZ = 6, + ZXZ = 7, + XZX = 8, + XYX = 9, + ZSXX = 10, + ZSX = 11, +}; +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/EulerDecomposition.h b/mlir/include/mlir/Passes/Decomposition/EulerDecomposition.h new file mode 100644 index 000000000..35220464c --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/EulerDecomposition.h @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "EulerBasis.h" +#include "GateSequence.h" +#include "Helpers.h" +#include "ir/Definitions.hpp" +#include "ir/operations/OpType.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +/** + * Decomposition of single-qubit matrices into rotation gates using a KAK + * decomposition. + */ +class EulerDecomposition { +public: + /** + * Perform single-qubit decomposition of a 2x2 unitary matrix based on a + * given euler basis. + */ + [[nodiscard]] static OneQubitGateSequence + generateCircuit(EulerBasis targetBasis, const matrix2x2& unitaryMatrix, + bool simplify, std::optional atol) { + auto [theta, phi, lambda, phase] = + anglesFromUnitary(unitaryMatrix, targetBasis); + + switch (targetBasis) { + case EulerBasis::ZYZ: + return decomposeKAK(theta, phi, lambda, phase, qc::RZ, qc::RY, simplify, + atol); + case EulerBasis::ZXZ: + return decomposeKAK(theta, phi, lambda, phase, qc::RZ, qc::RX, simplify, + atol); + case EulerBasis::XZX: + return decomposeKAK(theta, phi, lambda, phase, qc::RX, qc::RZ, simplify, + atol); + case EulerBasis::XYX: + return decomposeKAK(theta, phi, lambda, phase, qc::RX, qc::RY, simplify, + atol); + default: + // TODO: allow other bases + throw std::invalid_argument{"Unsupported base for circuit generation!"}; + } + } + + /** + * Calculate angles of a single-qubit matrix according to the given + * EulerBasis. + * + * @return array containing (theta, phi, lambda, phase) in this order + */ + static std::array anglesFromUnitary(const matrix2x2& matrix, + EulerBasis basis) { + if (basis == EulerBasis::XYX) { + return paramsXyxInner(matrix); + } + if (basis == EulerBasis::XZX) { + return paramsXzxInner(matrix); + } + if (basis == EulerBasis::ZYZ) { + return paramsZyzInner(matrix); + } + if (basis == EulerBasis::ZXZ) { + return paramsZxzInner(matrix); + } + throw std::invalid_argument{"Unknown EulerBasis for angles_from_unitary"}; + } + +private: + static std::array paramsZyzInner(const matrix2x2& matrix) { + const auto detArg = std::arg(matrix.determinant()); + const auto phase = 0.5 * detArg; + const auto theta = + 2. * std::atan2(std::abs(matrix(1, 0)), std::abs(matrix(0, 0))); + const auto ang1 = std::arg(matrix(1, 1)); + const auto ang2 = std::arg(matrix(1, 0)); + const auto phi = ang1 + ang2 - detArg; + const auto lam = ang1 - ang2; + return {theta, phi, lam, phase}; + } + + static std::array paramsZxzInner(const matrix2x2& matrix) { + const auto [theta, phi, lam, phase] = paramsZyzInner(matrix); + return {theta, phi + (qc::PI / 2.), lam - (qc::PI / 2.), phase}; + } + + static std::array paramsXyxInner(const matrix2x2& matrix) { + const matrix2x2 matZyz{ + {static_cast(0.5) * + (matrix(0, 0) + matrix(0, 1) + matrix(1, 0) + matrix(1, 1)), + static_cast(0.5) * + (matrix(0, 0) - matrix(0, 1) + matrix(1, 0) - matrix(1, 1))}, + {static_cast(0.5) * + (matrix(0, 0) + matrix(0, 1) - matrix(1, 0) - matrix(1, 1)), + static_cast(0.5) * + (matrix(0, 0) - matrix(0, 1) - matrix(1, 0) + matrix(1, 1))}, + }; + auto [theta, phi, lam, phase] = paramsZyzInner(matZyz); + auto newPhi = helpers::mod2pi(phi + qc::PI, 0.); + auto newLam = helpers::mod2pi(lam + qc::PI, 0.); + return { + theta, + newPhi, + newLam, + phase + ((newPhi + newLam - phi - lam) / 2.), + }; + } + + static std::array paramsXzxInner(const matrix2x2& matrix) { + auto det = matrix.determinant(); + auto phase = std::imag(std::log(det)) / 2.0; + auto sqrtDet = std::sqrt(det); + const matrix2x2 matZyz{ + { + {(matrix(0, 0) / sqrtDet).real(), (matrix(1, 0) / sqrtDet).imag()}, + {(matrix(1, 0) / sqrtDet).real(), (matrix(0, 0) / sqrtDet).imag()}, + }, + { + {-(matrix(1, 0) / sqrtDet).real(), (matrix(0, 0) / sqrtDet).imag()}, + {(matrix(0, 0) / sqrtDet).real(), -(matrix(1, 0) / sqrtDet).imag()}, + }, + }; + auto [theta, phi, lam, phase_zxz] = paramsZxzInner(matZyz); + return {theta, phi, lam, phase + phase_zxz}; + } + + /** + * @note Adapted from circuit_kak() in the IBM Qiskit framework. + * (C) Copyright IBM 2022 + * + * This code is licensed under the Apache License, Version 2.0. You + * may obtain a copy of this license in the LICENSE.txt file in the root + * directory of this source tree or at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain + * this copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ + [[nodiscard]] static OneQubitGateSequence + decomposeKAK(fp theta, fp phi, fp lambda, fp phase, qc::OpType kGate, + qc::OpType aGate, bool simplify, std::optional atol) { + fp angleZeroEpsilon = atol.value_or(DEFAULT_ATOL); + if (!simplify) { + angleZeroEpsilon = -1.0; + } + + OneQubitGateSequence sequence{ + .gates = {}, + .globalPhase = phase - ((phi + lambda) / 2.), + }; + if (std::abs(theta) <= angleZeroEpsilon) { + lambda += phi; + lambda = helpers::mod2pi(lambda); + if (std::abs(lambda) > angleZeroEpsilon) { + sequence.gates.push_back({.type = kGate, .parameter = {lambda}}); + sequence.globalPhase += lambda / 2.0; + } + return sequence; + } + + if (std::abs(theta - qc::PI) <= angleZeroEpsilon) { + sequence.globalPhase += phi; + lambda -= phi; + phi = 0.0; + } + if (std::abs(helpers::mod2pi(lambda + qc::PI)) <= angleZeroEpsilon || + std::abs(helpers::mod2pi(phi + qc::PI)) <= angleZeroEpsilon) { + lambda += qc::PI; + theta = -theta; + phi += qc::PI; + } + lambda = helpers::mod2pi(lambda); + if (std::abs(lambda) > angleZeroEpsilon) { + sequence.globalPhase += lambda / 2.0; + sequence.gates.push_back({.type = kGate, .parameter = {lambda}}); + } + sequence.gates.push_back({.type = aGate, .parameter = {theta}}); + phi = helpers::mod2pi(phi); + if (std::abs(phi) > angleZeroEpsilon) { + sequence.globalPhase += phi / 2.0; + sequence.gates.push_back({.type = kGate, .parameter = {phi}}); + } + return sequence; + } +}; +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/Gate.h b/mlir/include/mlir/Passes/Decomposition/Gate.h new file mode 100644 index 000000000..f6ca1d3d8 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/Gate.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "Helpers.h" +#include "ir/operations/OpType.hpp" + +#include + +namespace mlir::qco::decomposition { + +using QubitId = std::size_t; + +/** + * Gate description which should be able to represent every possible + * one-qubit or two-qubit operation. + */ +struct Gate { + qc::OpType type{qc::I}; + llvm::SmallVector parameter; + llvm::SmallVector qubitId = {0}; +}; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/GateSequence.h b/mlir/include/mlir/Passes/Decomposition/GateSequence.h new file mode 100644 index 000000000..fd66133f4 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/GateSequence.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "EulerBasis.h" +#include "Gate.h" +#include "Helpers.h" +#include "UnitaryMatrices.h" +#include "ir/operations/OpType.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { +/** + * Gate sequence of single-qubit and/or two-qubit gates. + */ +struct QubitGateSequence { + /** + * Container sorting the gate sequence in order. + */ + llvm::SmallVector gates; + + /** + * Global phase adjustment required for the sequence. + */ + fp globalPhase{}; + /** + * @return true if the global phase adjustment is not zero. + */ + [[nodiscard]] bool hasGlobalPhase() const { + return std::abs(globalPhase) > DEFAULT_ATOL; + } + + /** + * Calculate complexity of sequence according to getComplexity(). + */ + [[nodiscard]] std::size_t complexity() const { + // TODO: add more sophisticated metric to determine complexity of + // series/sequence + // TODO: caching mechanism + std::size_t c{}; + for (auto&& gate : gates) { + c += helpers::getComplexity(gate.type, gate.qubitId.size()); + } + if (hasGlobalPhase()) { + // need to add a global phase gate if a global phase needs to be applied + c += helpers::getComplexity(qc::GPhase, 0); + } + return c; + } + + /** + * Calculate overall unitary matrix of the sequence. + */ + [[nodiscard]] matrix4x4 getUnitaryMatrix() const { + matrix4x4 unitaryMatrix = + helpers::kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE); + for (auto&& gate : gates) { + auto gateMatrix = getTwoQubitMatrix(gate); + unitaryMatrix = gateMatrix * unitaryMatrix; + } + unitaryMatrix *= std::exp(IM * globalPhase); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; + } +}; +/** + * Helper type to show that a gate sequence is supposed to only contain + * single-qubit gates. + */ +using OneQubitGateSequence = QubitGateSequence; +/** + * Helper type to show that the gate sequence may contain two-qubit gates. + */ +using TwoQubitGateSequence = QubitGateSequence; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/Helpers.h b/mlir/include/mlir/Passes/Decomposition/Helpers.h new file mode 100644 index 000000000..44c4e5a31 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/Helpers.h @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "ir/Definitions.hpp" +#include "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/IR/QCODialect.h" + +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // TODO: unstable, NOLINT(misc-include-cleaner) + +namespace mlir::qco { +using fp = qc::fp; +using qfp = std::complex; +// NOLINTBEGIN(misc-include-cleaner) +using matrix2x2 = Eigen::Matrix2; +using matrix4x4 = Eigen::Matrix4; +using rmatrix4x4 = Eigen::Matrix4; +using diagonal4x4 = Eigen::Vector; +using rdiagonal4x4 = Eigen::Vector; +// NOLINTEND(misc-include-cleaner) + +constexpr qfp C_ZERO{0., 0.}; +constexpr qfp C_ONE{1., 0.}; +constexpr qfp C_M_ONE{-1., 0.}; +constexpr qfp IM{0., 1.}; +constexpr qfp M_IM{0., -1.}; + +} // namespace mlir::qco + +namespace mlir::qco::helpers { + +std::optional mlirValueToFp(mlir::Value value); + +template +[[nodiscard]] std::optional performMlirFloatBinaryOp(mlir::Value value, + Func&& func) { + if (auto op = value.getDefiningOp()) { + auto lhs = mlirValueToFp(op.getLhs()); + auto rhs = mlirValueToFp(op.getRhs()); + if (lhs && rhs) { + return std::invoke(std::forward(func), *lhs, *rhs); + } + } + return std::nullopt; +} + +template +[[nodiscard]] std::optional performMlirFloatUnaryOp(mlir::Value value, + Func&& func) { + if (auto op = value.getDefiningOp()) { + if (auto operand = mlirValueToFp(op.getOperand())) { + return std::invoke(std::forward(func), *operand); + } + } + return std::nullopt; +} + +[[nodiscard]] inline std::optional mlirValueToFp(mlir::Value value) { + if (auto op = value.getDefiningOp()) { + if (auto attr = llvm::dyn_cast(op.getValue())) { + return attr.getValueAsDouble(); + } + return std::nullopt; + } + if (auto result = performMlirFloatUnaryOp( + value, [](fp a) { return -a; })) { + return result; + } + if (auto result = performMlirFloatUnaryOp( + value, [](fp a) { return a; })) { + return result; + } + if (auto result = performMlirFloatUnaryOp( + value, [](fp a) { return a; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::max(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::max(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::min(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::min(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::fmod(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return a + b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return a * b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return a / b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return a - b; })) { + return result; + } + return std::nullopt; +} + +[[nodiscard]] inline llvm::SmallVector +getParameters(UnitaryOpInterface op) { + llvm::SmallVector parameters; + for (std::size_t i = 0; i < op.getNumParams(); ++i) { + if (auto value = helpers::mlirValueToFp(op.getParameter(i))) { + parameters.push_back(*value); + } + } + return parameters; +} + +[[nodiscard]] inline qc::OpType getQcType(UnitaryOpInterface op) { + try { + const std::string type = op->getName().stripDialect().str(); + return qc::opTypeFromString(type); + } catch (const std::invalid_argument& /*exception*/) { + return qc::OpType::None; + } +} + +[[nodiscard]] inline bool isSingleQubitOperation(UnitaryOpInterface op) { + return op.isSingleQubit(); +} + +[[nodiscard]] inline bool isTwoQubitOperation(UnitaryOpInterface op) { + return op.isTwoQubit(); +} + +// NOLINTBEGIN(misc-include-cleaner) +template +[[nodiscard]] inline Eigen::Matrix4 +kroneckerProduct(const Eigen::Matrix2& lhs, const Eigen::Matrix2& rhs) { + return Eigen::kroneckerProduct(lhs, rhs); +} + +template +[[nodiscard]] inline auto selfAdjointEvd(Eigen::Matrix a) { + Eigen::SelfAdjointEigenSolver s; + s.compute(a); // TODO: computeDirect is faster + auto vecs = s.eigenvectors().eval(); + auto vals = s.eigenvalues(); + return std::make_pair(vecs, vals); +} + +template +[[nodiscard]] bool isUnitaryMatrix(const Eigen::Matrix& matrix) { + return (matrix.transpose().conjugate() * matrix).isIdentity(); +} +// NOLINTEND(misc-include-cleaner) + +[[nodiscard]] inline fp remEuclid(fp a, fp b) { + auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +// Wrap angle into interval [-π,π). If within atol of the endpoint, clamp +// to -π +[[nodiscard]] inline fp mod2pi(fp angle, fp angleZeroEpsilon = 1e-13) { + // remEuclid() isn't exactly the same as Python's % operator, but + // because the RHS here is a constant and positive it is effectively + // equivalent for this case + auto wrapped = remEuclid(angle + qc::PI, qc::TAU) - qc::PI; + if (std::abs(wrapped - qc::PI) < angleZeroEpsilon) { + return -qc::PI; + } + return wrapped; +} + +[[nodiscard]] inline fp traceToFidelity(const qfp& x) { + auto xAbs = std::abs(x); + return (4.0 + xAbs * xAbs) / 20.0; +} + +[[nodiscard]] inline std::size_t getComplexity(qc::OpType type, + std::size_t numOfQubits) { + if (numOfQubits > 1) { + constexpr std::size_t multiQubitFactor = 10; + return (numOfQubits - 1) * multiQubitFactor; + } + if (type == qc::GPhase) { + return 2; + } + return 1; +} + +} // namespace mlir::qco::helpers diff --git a/mlir/include/mlir/Passes/Decomposition/UnitaryMatrices.h b/mlir/include/mlir/Passes/Decomposition/UnitaryMatrices.h new file mode 100644 index 000000000..322112990 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/UnitaryMatrices.h @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "Gate.h" +#include "Helpers.h" +#include "ir/operations/OpType.hpp" + +namespace mlir::qco::decomposition { + +inline constexpr auto SQRT2 = + static_cast(1.414213562373095048801688724209698079L); +inline constexpr auto FRAC1_SQRT2 = static_cast( + 0.707106781186547524400844362104849039284835937688474036588L); + +[[nodiscard]] inline matrix2x2 uMatrix(const fp lambda, const fp phi, + const fp theta) { + return matrix2x2{{{{std::cos(theta / 2.), 0.}, + {-std::cos(lambda) * std::sin(theta / 2.), + -std::sin(lambda) * std::sin(theta / 2.)}}, + {{std::cos(phi) * std::sin(theta / 2.), + std::sin(phi) * std::sin(theta / 2.)}, + {std::cos(lambda + phi) * std::cos(theta / 2.), + std::sin(lambda + phi) * std::cos(theta / 2.)}}}}; +} + +[[nodiscard]] inline matrix2x2 u2Matrix(const fp lambda, const fp phi) { + return matrix2x2{ + {FRAC1_SQRT2, + {-std::cos(lambda) * FRAC1_SQRT2, -std::sin(lambda) * FRAC1_SQRT2}}, + {{std::cos(phi) * FRAC1_SQRT2, std::sin(phi) * FRAC1_SQRT2}, + {std::cos(lambda + phi) * FRAC1_SQRT2, + std::sin(lambda + phi) * FRAC1_SQRT2}}}; +} + +inline matrix2x2 rxMatrix(fp theta) { + auto halfTheta = theta / 2.; + auto cos = qfp(std::cos(halfTheta), 0.); + auto isin = qfp(0., -std::sin(halfTheta)); + return matrix2x2{{cos, isin}, {isin, cos}}; +} + +inline matrix2x2 ryMatrix(fp theta) { + auto halfTheta = theta / 2.; + auto cos = qfp(std::cos(halfTheta), 0.); + auto sin = qfp(std::sin(halfTheta), 0.); + return matrix2x2{{cos, -sin}, {sin, cos}}; +} + +inline matrix2x2 rzMatrix(fp theta) { + return matrix2x2{{qfp{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0}, + {0, qfp{std::cos(theta / 2.), std::sin(theta / 2.)}}}; +} + +inline matrix4x4 rxxMatrix(const fp theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return matrix4x4{{cosTheta, C_ZERO, C_ZERO, {0., -sinTheta}}, + {C_ZERO, cosTheta, {0., -sinTheta}, C_ZERO}, + {C_ZERO, {0., -sinTheta}, cosTheta, C_ZERO}, + {{0., -sinTheta}, C_ZERO, C_ZERO, cosTheta}}; +} + +inline matrix4x4 ryyMatrix(const fp theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return matrix4x4{{{cosTheta, 0, 0, {0., sinTheta}}, + {0, cosTheta, {0., -sinTheta}, 0}, + {0, {0., -sinTheta}, cosTheta, 0}, + {{0., sinTheta}, 0, 0, cosTheta}}}; +} + +inline matrix4x4 rzzMatrix(const fp theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return matrix4x4{{qfp{cosTheta, -sinTheta}, C_ZERO, C_ZERO, C_ZERO}, + {C_ZERO, {cosTheta, sinTheta}, C_ZERO, C_ZERO}, + {C_ZERO, C_ZERO, {cosTheta, sinTheta}, C_ZERO}, + {C_ZERO, C_ZERO, C_ZERO, {cosTheta, -sinTheta}}}; +} + +inline matrix2x2 pMatrix(const fp lambda) { + return matrix2x2{{1, 0}, {0, {std::cos(lambda), std::sin(lambda)}}}; +} +const matrix2x2 IDENTITY_GATE = matrix2x2::Identity(); +const matrix2x2 H_GATE{{1.0 / SQRT2, 1.0 / SQRT2}, {1.0 / SQRT2, -1.0 / SQRT2}}; +const matrix2x2 IPZ{{IM, C_ZERO}, {C_ZERO, M_IM}}; +const matrix2x2 IPY{{C_ZERO, C_ONE}, {C_M_ONE, C_ZERO}}; +const matrix2x2 IPX{{C_ZERO, IM}, {IM, C_ZERO}}; + +inline matrix2x2 getSingleQubitMatrix(const Gate& gate) { + if (gate.type == qc::SX) { + return matrix2x2{{qfp{0.5, 0.5}, qfp{0.5, -0.5}}, + {qfp{0.5, -0.5}, qfp{0.5, 0.5}}}; + } + if (gate.type == qc::RX) { + return rxMatrix(gate.parameter[0]); + } + if (gate.type == qc::RY) { + return ryMatrix(gate.parameter[0]); + } + if (gate.type == qc::RZ) { + return rzMatrix(gate.parameter[0]); + } + if (gate.type == qc::X) { + return matrix2x2{{0, 1}, {1, 0}}; + } + if (gate.type == qc::I) { + return IDENTITY_GATE; + } + if (gate.type == qc::P) { + return pMatrix(gate.parameter[0]); + } + if (gate.type == qc::U) { + return uMatrix(gate.parameter[0], gate.parameter[1], gate.parameter[2]); + } + if (gate.type == qc::U2) { + return u2Matrix(gate.parameter[0], gate.parameter[1]); + } + if (gate.type == qc::H) { + return matrix2x2{{FRAC1_SQRT2, FRAC1_SQRT2}, {FRAC1_SQRT2, -FRAC1_SQRT2}}; + } + throw std::invalid_argument{ + "unsupported gate type for single qubit matrix (" + + qc::toString(gate.type) + ")"}; +} + +inline matrix4x4 getTwoQubitMatrix(const Gate& gate) { + using helpers::kroneckerProduct; + + if (gate.qubitId.empty()) { + return kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE); + } + if (gate.qubitId.size() == 1) { + if (gate.qubitId[0] == 0) { + return kroneckerProduct(IDENTITY_GATE, getSingleQubitMatrix(gate)); + } + if (gate.qubitId[0] == 1) { + return kroneckerProduct(getSingleQubitMatrix(gate), IDENTITY_GATE); + } + throw std::logic_error{"Invalid qubit ID in getTwoQubitMatrix"}; + } + if (gate.qubitId.size() == 2) { + if (gate.type == qc::X) { + // controlled X (CX) + if (gate.qubitId == llvm::SmallVector{0, 1}) { + return matrix4x4{ + {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}}; + } + if (gate.qubitId == llvm::SmallVector{1, 0}) { + return matrix4x4{ + {1, 0, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}, {0, 1, 0, 0}}; + } + } + if (gate.type == qc::RXX) { + // TODO: check qubit order? + return rxxMatrix(gate.parameter[0]); + } + if (gate.type == qc::RYY) { + // TODO: check qubit order? + return ryyMatrix(gate.parameter[0]); + } + if (gate.type == qc::RZZ) { + // TODO: check qubit order? + return rzzMatrix(gate.parameter[0]); + } + if (gate.type == qc::I) { + return kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE); + } + throw std::invalid_argument{"unsupported gate type for two qubit matrix (" + + qc::toString(gate.type) + ")"}; + } + throw std::logic_error{"Invalid number of qubit IDs in compute_unitary"}; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/WeylDecomposition.h b/mlir/include/mlir/Passes/Decomposition/WeylDecomposition.h new file mode 100644 index 000000000..ad37ab137 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/WeylDecomposition.h @@ -0,0 +1,784 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "EulerBasis.h" +#include "EulerDecomposition.h" +#include "Helpers.h" +#include "UnitaryMatrices.h" +#include "ir/Definitions.hpp" +#include "ir/operations/OpType.hpp" + +#include // NOLINT(misc-include-cleaner) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // TODO: unstable, NOLINT(misc-include-cleaner) +#include + +namespace mlir::qco::decomposition { +/** + * Allowed deviation for internal assert statements which ensure the correctness + * of the decompositions. + */ +constexpr fp SANITY_CHECK_PRECISION = 1e-12; + +/** + * Weyl decomposition of a 2-qubit unitary matrix (4x4). + * The result consists of four 2x2 1-qubit matrices (k1l, k2l, k1r, k2r) and + * three parameters for a canonical gate (a, b, c). The matrices can then be + * decomposed using a single-qubit decomposition into e.g. rotation gates and + * the canonical gate is RXX(-2 * a), RYY(-2 * b), RZZ (-2 * c). + */ +struct TwoQubitWeylDecomposition { + enum class Specialization : std::uint8_t { + General, // canonical gate has no special symmetry. + IdEquiv, // canonical gate is identity. + SWAPEquiv, // canonical gate is SWAP. + PartialSWAPEquiv, // canonical gate is partial SWAP. + PartialSWAPFlipEquiv, // canonical gate is flipped partial SWAP. + ControlledEquiv, // canonical gate is a controlled gate. + MirrorControlledEquiv, // canonical gate is swap + controlled gate. + + // These next 3 gates use the definition of fSim from eq (1) in: + // https://arxiv.org/pdf/2001.08343.pdf + FSimaabEquiv, // parameters a=b & a!=c + FSimabbEquiv, // parameters a!=b & b=c + FSimabmbEquiv, // parameters a!=b!=c & -b=c + }; + + // a, b, c are the parameters of the canonical gate (CAN) + fp a; // rotation of RXX gate in CAN (must be taken times -2.0) + fp b; // rotation of RYY gate in CAN (must be taken times -2.0) + fp c; // rotation of RZZ gate in CAN (must be taken times -2.0) + fp globalPhase; // global phase adjustment + /** + * q1 - k2r - C - k1r - + * A + * q0 - k2l - N - k1l - + */ + matrix2x2 k1l; // "left" qubit after canonical gate + matrix2x2 k2l; // "left" qubit before canonical gate + matrix2x2 k1r; // "right" qubit after canonical gate + matrix2x2 k2r; // "right" qubit before canonical gate + Specialization specialization; // detected symmetries in the matrix + EulerBasis defaultEulerBasis; // recommended euler basis for k1l/k2l/k1r/k2r + std::optional requestedFidelity; // desired fidelity; + // if set to std::nullopt, no automatic + // specialization will be applied + fp calculatedFidelity; // actual fidelity of decomposition + matrix4x4 unitaryMatrix; // original matrix for this decomposition + + /** + * Create Weyl decomposition. + * + * @param unitaryMatrix Matrix of the two-qubit operation/series to be + * decomposed. + * @param fidelity Tolerance to assume a specialization which is used to + * reduce the number of parameters required by the canonical + * gate and thus potentially decreasing the number of basis + * gates. + */ + static TwoQubitWeylDecomposition create(const matrix4x4& unitaryMatrix, + std::optional fidelity) { + auto u = unitaryMatrix; + auto detU = u.determinant(); + auto detPow = std::pow(detU, static_cast(-0.25)); + u *= detPow; // remove global phase from unitary matrix + auto globalPhase = std::arg(detU) / 4.; + + // this should have normalized determinant of u, so that u ∈ SU(4) + assert(std::abs(u.determinant() - 1.0) < SANITY_CHECK_PRECISION); + + // transform unitary matrix to magic basis; this enables two properties: + // 1. if uP ∈ SO(4), V = A ⊗ B (SO(4) → SU(2) ⊗ SU(2)) + // 2. magic basis diagonalizes canonical gate, allowing calculation of + // canonical gate parameters later on + auto uP = magicBasisTransform(u, MagicBasisTransform::OutOf); + const matrix4x4 m2 = uP.transpose() * uP; + + // diagonalization yields eigenvectors (p) and eigenvalues (d); + // p is used to calculate K1/K2 (and thus the single-qubit gates + // surrounding the canonical gate); d is is used to determine the weyl + // coordinates and thus the parameters of the canonical gate + // TODO: it may be possible to lower the precision + auto [p, d] = diagonalizeComplexSymmetric(m2, 1e-13); + + // extract Weyl coordinates from eigenvalues, map to [0, 2*pi) + // NOLINTNEXTLINE(misc-include-cleaner) + Eigen::Vector cs; + rdiagonal4x4 dReal = -1.0 * d.cwiseArg() / 2.0; + dReal(3) = -dReal(0) - dReal(1) - dReal(2); + for (int i = 0; i < static_cast(cs.size()); ++i) { + assert(i < dReal.size()); + cs[i] = helpers::remEuclid((dReal(i) + dReal(3)) / 2.0, qc::TAU); + } + + // re-order coordinates and according to min(a, pi/2 - a) with + // a = x mod pi/2 for each weyl coordinate x + decltype(cs) cstemp; + llvm::transform(cs, cstemp.begin(), [](auto&& x) { + auto tmp = helpers::remEuclid(x, qc::PI_2); + return std::min(tmp, qc::PI_2 - tmp); + }); + std::array order{0, 1, 2}; + llvm::stable_sort(order, + [&](auto a, auto b) { return cstemp[a] < cstemp[b]; }); + std::tie(order[0], order[1], order[2]) = + std::tuple{order[1], order[2], order[0]}; + std::tie(cs[0], cs[1], cs[2]) = + std::tuple{cs[order[0]], cs[order[1]], cs[order[2]]}; + std::tie(dReal(0), dReal(1), dReal(2)) = + std::tuple{dReal(order[0]), dReal(order[1]), dReal(order[2])}; + + // update eigenvectors (columns of p) according to new order of + // weyl coordinates + matrix4x4 pOrig = p; + for (int i = 0; std::cmp_less(i, order.size()); ++i) { + p.col(i) = pOrig.col(order[i]); + } + // apply correction for determinant if necessary + if (p.determinant().real() < 0.0) { + auto lastColumnIndex = p.cols() - 1; + p.col(lastColumnIndex) *= -1.0; + } + assert(std::abs(p.determinant() - 1.0) < SANITY_CHECK_PRECISION); + + // re-create complex eigenvalue matrix; this matrix contains the + // parameters of the canonical gate which is later used in the + // verification + matrix4x4 temp = dReal.asDiagonal(); + temp *= IM; + temp = temp.exp(); + + // combined matrix k1 of 1q gates after canonical gate + matrix4x4 k1 = uP * p * temp; + assert((k1.transpose() * k1).isIdentity()); // k1 must be orthogonal + assert(k1.determinant().real() > 0.0); + k1 = magicBasisTransform(k1, MagicBasisTransform::Into); + + // combined matrix k2 of 1q gates before canonical gate + matrix4x4 k2 = p.transpose().conjugate(); + assert((k2.transpose() * k2).isIdentity()); // k2 must be orthogonal + assert(k2.determinant().real() > 0.0); + k2 = magicBasisTransform(k2, MagicBasisTransform::Into); + + // ensure k1 and k2 are correct (when combined with the canonical gate + // parameters in-between, they are equivalent to u) + assert( + (k1 * magicBasisTransform(temp.conjugate(), MagicBasisTransform::Into) * + k2) + .isApprox(u, SANITY_CHECK_PRECISION)); + + // calculate k1 = K1l ⊗ K1r + auto [K1l, K1r, phase_l] = decomposeTwoQubitProductGate(k1); + // decompose k2 = K2l ⊗ K2r + auto [K2l, K2r, phase_r] = decomposeTwoQubitProductGate(k2); + assert(helpers::kroneckerProduct(K1l, K1r).isApprox( + k1, SANITY_CHECK_PRECISION)); + assert(helpers::kroneckerProduct(K2l, K2r).isApprox( + k2, SANITY_CHECK_PRECISION)); + // accumulate global phase + globalPhase += phase_l + phase_r; + + // Flip into Weyl chamber + if (cs[0] > qc::PI_2) { + cs[0] -= 3.0 * qc::PI_2; + K1l = K1l * IPY; + K1r = K1r * IPY; + globalPhase += qc::PI_2; + } + if (cs[1] > qc::PI_2) { + cs[1] -= 3.0 * qc::PI_2; + K1l = K1l * IPX; + K1r = K1r * IPX; + globalPhase += qc::PI_2; + } + auto conjs = 0; + if (cs[0] > qc::PI_4) { + cs[0] = qc::PI_2 - cs[0]; + K1l = K1l * IPY; + K2r = IPY * K2r; + conjs += 1; + globalPhase -= qc::PI_2; + } + if (cs[1] > qc::PI_4) { + cs[1] = qc::PI_2 - cs[1]; + K1l = K1l * IPX; + K2r = IPX * K2r; + conjs += 1; + globalPhase += qc::PI_2; + if (conjs == 1) { + globalPhase -= qc::PI; + } + } + if (cs[2] > qc::PI_2) { + cs[2] -= 3.0 * qc::PI_2; + K1l = K1l * IPZ; + K1r = K1r * IPZ; + globalPhase += qc::PI_2; + if (conjs == 1) { + globalPhase -= qc::PI; + } + } + if (conjs == 1) { + cs[2] = qc::PI_2 - cs[2]; + K1l = K1l * IPZ; + K2r = IPZ * K2r; + globalPhase += qc::PI_2; + } + if (cs[2] > qc::PI_4) { + cs[2] -= qc::PI_2; + K1l = K1l * IPZ; + K1r = K1r * IPZ; + globalPhase -= qc::PI_2; + } + + // bind weyl coordinates as parameters of canonical gate + auto [a, b, c] = std::tie(cs[1], cs[0], cs[2]); + + TwoQubitWeylDecomposition decomposition{ + .a = a, + .b = b, + .c = c, + .globalPhase = globalPhase, + .k1l = K1l, + .k2l = K2l, + .k1r = K1r, + .k2r = K2r, + .specialization = Specialization::General, + .defaultEulerBasis = EulerBasis::ZYZ, + .requestedFidelity = fidelity, + // will be calculated if a specialization is used, set to -1 for now + .calculatedFidelity = -1.0, + .unitaryMatrix = unitaryMatrix, + }; + // make sure decomposition is equal to input + assert((helpers::kroneckerProduct(K1l, K1r) * + decomposition.getCanonicalMatrix() * + helpers::kroneckerProduct(K2l, K2r) * std::exp(IM * globalPhase)) + .isApprox(unitaryMatrix, SANITY_CHECK_PRECISION)); + + // determine actual specialization of canonical gate so that the 1q + // matrices can potentially be simplified + auto flippedFromOriginal = decomposition.applySpecialization(); + + auto getTrace = [&]() { + if (flippedFromOriginal) { + return TwoQubitWeylDecomposition::getTrace( + qc::PI_2 - a, b, -c, decomposition.a, decomposition.b, + decomposition.c); + } + return TwoQubitWeylDecomposition::getTrace( + a, b, c, decomposition.a, decomposition.b, decomposition.c); + }; + // use trace to calculate fidelity of applied specialization and + // adjust global phase + auto trace = getTrace(); + decomposition.calculatedFidelity = helpers::traceToFidelity(trace); + // final check if specialization is close enough to the original matrix to + // satisfy the requested fidelity; since no forced specialization is + // allowed, this should never fail + if (decomposition.requestedFidelity) { + if (decomposition.calculatedFidelity + 1.0e-13 < + *decomposition.requestedFidelity) { + throw std::runtime_error{ + "TwoQubitWeylDecomposition: Calculated fidelity of " + "specialization is worse than requested fidelity!"}; + } + } + decomposition.globalPhase += std::arg(trace); + + // final check if decomposition is still valid after specialization + assert((helpers::kroneckerProduct(decomposition.k1l, decomposition.k1r) * + decomposition.getCanonicalMatrix() * + helpers::kroneckerProduct(decomposition.k2l, decomposition.k2r) * + std::exp(IM * decomposition.globalPhase)) + .isApprox(unitaryMatrix, SANITY_CHECK_PRECISION)); + + return decomposition; + } + + /** + * Calculate matrix of canonical gate based on its parameters a, b, c. + */ + [[nodiscard]] matrix4x4 getCanonicalMatrix() const { + auto xx = getTwoQubitMatrix({ + .type = qc::RXX, + .parameter = {-2.0 * a}, + .qubitId = {0, 1}, + }); + auto yy = getTwoQubitMatrix({ + .type = qc::RYY, + .parameter = {-2.0 * b}, + .qubitId = {0, 1}, + }); + auto zz = getTwoQubitMatrix({ + .type = qc::RZZ, + .parameter = {-2.0 * c}, + .qubitId = {0, 1}, + }); + return zz * yy * xx; + } + +protected: + static constexpr fp SANITY_CHECK_PRECISION = 1e-12; + + // https://docs.rs/faer/latest/faer/mat/generic/struct.Mat.html#method.self_adjoint_eigen + template static auto selfAdjointEigenLower(T&& a) { + auto [U, S] = helpers::selfAdjointEvd(std::forward(a)); + + return std::make_pair(U, S); + } + + enum class MagicBasisTransform : std::uint8_t { + Into, + OutOf, + }; + + static matrix4x4 magicBasisTransform(const matrix4x4& unitary, + MagicBasisTransform direction) { + const matrix4x4 bNonNormalized{ + {C_ONE, IM, C_ZERO, C_ZERO}, + {C_ZERO, C_ZERO, IM, C_ONE}, + {C_ZERO, C_ZERO, IM, C_M_ONE}, + {C_ONE, M_IM, C_ZERO, C_ZERO}, + }; + + const matrix4x4 bNonNormalizedDagger{ + {qfp(0.5, 0.), C_ZERO, C_ZERO, qfp(0.5, 0.)}, + {qfp(0., -0.5), C_ZERO, C_ZERO, qfp(0., 0.5)}, + {C_ZERO, qfp(0., -0.5), qfp(0., -0.5), C_ZERO}, + {C_ZERO, qfp(0.5, 0.), qfp(-0.5, 0.), C_ZERO}, + }; + if (direction == MagicBasisTransform::OutOf) { + return bNonNormalizedDagger * unitary * bNonNormalized; + } + if (direction == MagicBasisTransform::Into) { + return bNonNormalized * unitary * bNonNormalizedDagger; + } + throw std::logic_error{"Unknown MagicBasisTransform direction!"}; + } + + static fp closestPartialSwap(fp a, fp b, fp c) { + auto m = (a + b + c) / 3.; + auto [am, bm, cm] = std::array{a - m, b - m, c - m}; + auto [ab, bc, ca] = std::array{a - b, b - c, c - a}; + return m + (am * bm * cm * (6. + ab * ab + bc * bc + ca * ca) / 18.); + } + + /** + * Diagonalize given complex symmetric matrix M into (P, d) using a + * randomized algorithm. + * This approach is used in both qiskit and quantumflow. + * + * P is the matrix of real or orthogonal eigenvectors of M with P ∈ SO(4) + * d is a vector containing sqrt(eigenvalues) of M with unit-magnitude + * elements (for each element, complex magnitude is 1.0). + * D is d as a diagonal matrix. + * + * M = P * D * P^T + * + * @return pair of (P, D.diagonal()) + */ + [[nodiscard]] static std::pair + diagonalizeComplexSymmetric(const matrix4x4& m, fp precision) { + // We can't use raw `eig` directly because it isn't guaranteed to give + // us real or orthogonal eigenvectors. Instead, since `M` is + // complex-symmetric, + // M = A + iB + // for real-symmetric `A` and `B`, and as + // M^+ @ M2 = A^2 + B^2 + i [A, B] = 1 + // we must have `A` and `B` commute, and consequently they are + // simultaneously diagonalizable. Mixing them together _should_ account + // for any degeneracy problems, but it's not guaranteed, so we repeat it + // a little bit. The fixed seed is to make failures deterministic; the + // value is not important. + auto state = std::mt19937{2023}; + std::normal_distribution dist; + + for (int i = 0; i < 100; ++i) { + fp randA{}; + fp randB{}; + // For debugging the algorithm use the same RNG values as the + // Qiskit implementation for the first random trial. + // In most cases this loop only executes a single iteration and + // using the same rng values rules out possible RNG differences + // as the root cause of a test failure + if (i == 0) { + randA = 1.2602066112249388; + randB = 0.22317849046722027; + } else { + randA = dist(state); + randB = dist(state); + } + const rmatrix4x4 m2Real = randA * m.real() + randB * m.imag(); + const rmatrix4x4 pReal = selfAdjointEigenLower(m2Real).first; + const matrix4x4 p = pReal; + const diagonal4x4 d = (p.transpose() * m * p).diagonal(); + + const matrix4x4 diagD = d.asDiagonal(); + + const matrix4x4 compare = p * diagD * p.transpose(); + if (compare.isApprox(m, precision)) { + // p are the eigenvectors which are decomposed into the + // single-qubit gates surrounding the canonical gate + // d is the sqrt of the eigenvalues that are used to determine the + // weyl coordinates and thus the parameters of the canonical gate + // check that p is in SO(4) + assert((p.transpose() * p).isIdentity(SANITY_CHECK_PRECISION)); + // make sure determinant of eigenvalues is 1.0 + assert(std::abs(matrix4x4{d.asDiagonal()}.determinant() - 1.0) < + SANITY_CHECK_PRECISION); + return std::make_pair(p, d); + } + } + throw std::runtime_error{ + "TwoQubitWeylDecomposition: failed to diagonalize M2."}; + } + + /** + * Decompose a special unitary matrix C that is the combination of two + * single-qubit gates A and B into its single-qubit matrices. + * + * C = A ⊗ B + * + * @param specialUnitary Special unitary matrix C + * + * @return single-qubit matrices A and B and the required + * global phase adjustment + */ + static std::tuple + decomposeTwoQubitProductGate(const matrix4x4& specialUnitary) { + // for alternative approaches, see + // pennylane's math.decomposition.su2su2_to_tensor_products + // or quantumflow.kronecker_decomposition + + // first quadrant + matrix2x2 r{{specialUnitary(0, 0), specialUnitary(0, 1)}, + {specialUnitary(1, 0), specialUnitary(1, 1)}}; + auto detR = r.determinant(); + if (std::abs(detR) < 0.1) { + // third quadrant + r = matrix2x2{{specialUnitary(2, 0), specialUnitary(2, 1)}, + {specialUnitary(3, 0), specialUnitary(3, 1)}}; + detR = r.determinant(); + } + if (std::abs(detR) < 0.1) { + throw std::runtime_error{ + "decompose_two_qubit_product_gate: unable to decompose: det_r < 0.1"}; + } + r /= std::sqrt(detR); + // transpose with complex conjugate of each element + const matrix2x2 rTConj = r.transpose().conjugate(); + + auto temp = helpers::kroneckerProduct(IDENTITY_GATE, rTConj); + temp = specialUnitary * temp; + + // [[a, b, c, d], + // [e, f, g, h], => [[a, c], + // [i, j, k, l], [i, k]] + // [m, n, o, p]] + matrix2x2 l{{temp(0, 0), temp(0, 2)}, {temp(2, 0), temp(2, 2)}}; + auto detL = l.determinant(); + if (std::abs(detL) < 0.9) { + throw std::runtime_error{ + "decompose_two_qubit_product_gate: unable to decompose: detL < 0.9"}; + } + l /= std::sqrt(detL); + auto phase = std::arg(detL) / 2.; + + return {l, r, phase}; + } + + /** + * Calculate trace of two sets of parameters for the canonical gate. + * The trace has been defined in: https://arxiv.org/abs/1811.12926 + */ + [[nodiscard]] static qfp getTrace(fp a, fp b, fp c, fp ap, fp bp, fp cp) { + auto da = a - ap; + auto db = b - bp; + auto dc = c - cp; + return static_cast(4.) * + qfp(std::cos(da) * std::cos(db) * std::cos(dc), + std::sin(da) * std::sin(db) * std::sin(dc)); + } + + /** + * Choose the best specialization for the for the canonical gate. + * This will use the requestedFidelity to determine if a specialization is + * close enough to the actual canonical gate matrix. + */ + [[nodiscard]] Specialization bestSpecialization() const { + auto isClose = [this](fp ap, fp bp, fp cp) -> bool { + auto tr = getTrace(a, b, c, ap, bp, cp); + if (requestedFidelity) { + return helpers::traceToFidelity(tr) >= *requestedFidelity; + } + return false; + }; + + auto closestAbc = closestPartialSwap(a, b, c); + auto closestAbMinusC = closestPartialSwap(a, b, -c); + + if (isClose(0., 0., 0.)) { + return Specialization::IdEquiv; + } + if (isClose(qc::PI_4, qc::PI_4, qc::PI_4) || + isClose(qc::PI_4, qc::PI_4, -qc::PI_4)) { + return Specialization::SWAPEquiv; + } + if (isClose(closestAbc, closestAbc, closestAbc)) { + return Specialization::PartialSWAPEquiv; + } + if (isClose(closestAbMinusC, closestAbMinusC, -closestAbMinusC)) { + return Specialization::PartialSWAPFlipEquiv; + } + if (isClose(a, 0., 0.)) { + return Specialization::ControlledEquiv; + } + if (isClose(qc::PI_4, qc::PI_4, c)) { + return Specialization::MirrorControlledEquiv; + } + if (isClose((a + b) / 2., (a + b) / 2., c)) { + return Specialization::FSimaabEquiv; + } + if (isClose(a, (b + c) / 2., (b + c) / 2.)) { + return Specialization::FSimabbEquiv; + } + if (isClose(a, (b - c) / 2., (c - b) / 2.)) { + return Specialization::FSimabmbEquiv; + } + return Specialization::General; + } + + /** + * @return true if the specialization flipped the original decomposition + */ + bool applySpecialization() { + if (specialization != Specialization::General) { + throw std::logic_error{"Application of specialization only works on " + "general decomposition!"}; + } + bool flippedFromOriginal = false; + auto newSpecialization = bestSpecialization(); + if (newSpecialization == Specialization::General) { + // U has no special symmetry. + // + // This gate binds all 6 possible parameters, so there is no need to + // make the single-qubit pre-/post-gates canonical. + return flippedFromOriginal; + } + specialization = newSpecialization; + + if (newSpecialization == Specialization::IdEquiv) { + // :math:`U \sim U_d(0,0,0)` + // Thus, :math:`\sim Id` + // + // This gate binds 0 parameters, we make it canonical by setting: + // + // :math:`K2_l = Id` , :math:`K2_r = Id`. + a = 0.; + b = 0.; + c = 0.; + // unmodified global phase + k1l = k1l * k2l; + k2l = IDENTITY_GATE; + k1r = k1r * k2r; + k2r = IDENTITY_GATE; + } else if (newSpecialization == Specialization::SWAPEquiv) { + // :math:`U \sim U_d(\pi/4, \pi/4, \pi/4) \sim U(\pi/4, \pi/4, -\pi/4)` + // Thus, :math:`U \sim \text{SWAP}` + // + // This gate binds 0 parameters, we make it canonical by setting: + // + // :math:`K2_l = Id` , :math:`K2_r = Id`. + a = qc::PI_4; + b = qc::PI_4; + c = qc::PI_4; + if (c > 0.) { + // unmodified global phase + k1l = k1l * k2r; + k1r = k1r * k2l; + k2l = IDENTITY_GATE; + k2r = IDENTITY_GATE; + } else { + flippedFromOriginal = true; + + globalPhase += qc::PI_2; + k1l = k1l * IPZ * k2r; + k1r = k1r * IPZ * k2l; + k2l = IDENTITY_GATE; + k2r = IDENTITY_GATE; + } + } else if (newSpecialization == Specialization::PartialSWAPEquiv) { + // :math:`U \sim U_d(\alpha\pi/4, \alpha\pi/4, \alpha\pi/4)` + // Thus, :math:`U \sim \text{SWAP}^\alpha` + // + // This gate binds 3 parameters, we make it canonical by setting: + // + // :math:`K2_l = Id`. + auto closest = closestPartialSwap(a, b, c); + auto k2lDagger = k2l.transpose().conjugate(); + + a = closest; + b = closest; + c = closest; + // unmodified global phase + k1l = k1l * k2l; + k1r = k1r * k2l; + k2r = k2lDagger * k2r; + k2l = IDENTITY_GATE; + } else if (newSpecialization == Specialization::PartialSWAPFlipEquiv) { + // :math:`U \sim U_d(\alpha\pi/4, \alpha\pi/4, -\alpha\pi/4)` + // Thus, :math:`U \sim \text{SWAP}^\alpha` + // + // (a non-equivalent root of SWAP from the TwoQubitWeylPartialSWAPEquiv + // similar to how :math:`x = (\pm \sqrt(x))^2`) + // + // This gate binds 3 parameters, we make it canonical by setting: + // + // :math:`K2_l = Id` + auto closest = closestPartialSwap(a, b, -c); + auto k2lDagger = k2l.transpose().conjugate(); + + a = closest; + b = closest; + c = -closest; + // unmodified global phase + k1l = k1l * k2l; + k1r = k1r * IPZ * k2l * IPZ; + k2r = IPZ * k2lDagger * IPZ * k2r; + k2l = IDENTITY_GATE; + } else if (newSpecialization == Specialization::ControlledEquiv) { + // :math:`U \sim U_d(\alpha, 0, 0)` + // Thus, :math:`U \sim \text{Ctrl-U}` + // + // This gate binds 4 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l) Rx(\lambda_l)` + // :math:`K2_r = Ry(\theta_r) Rx(\lambda_r)` + auto eulerBasis = EulerBasis::XYX; + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, eulerBasis); + auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + EulerDecomposition::anglesFromUnitary(k2r, eulerBasis); + + // unmodified parameter a + b = 0.; + c = 0.; + globalPhase = globalPhase + k2lphase + k2rphase; + k1l = k1l * rxMatrix(k2lphi); + k2l = ryMatrix(k2ltheta) * rxMatrix(k2llambda); + k1r = k1r * rxMatrix(k2rphi); + k2r = ryMatrix(k2rtheta) * rxMatrix(k2rlambda); + defaultEulerBasis = eulerBasis; + } else if (newSpecialization == Specialization::MirrorControlledEquiv) { + // :math:`U \sim U_d(\pi/4, \pi/4, \alpha)` + // Thus, :math:`U \sim \text{SWAP} \cdot \text{Ctrl-U}` + // + // This gate binds 4 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l)\cdot Rz(\lambda_l)` + // :math:`K2_r = Ry(\theta_r)\cdot Rz(\lambda_r)` + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, EulerBasis::ZYZ); + auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + EulerDecomposition::anglesFromUnitary(k2r, EulerBasis::ZYZ); + + a = qc::PI_4; + b = qc::PI_4; + // unmodified parameter c + globalPhase = globalPhase + k2lphase + k2rphase; + k1l = k1l * rzMatrix(k2rphi); + k2l = ryMatrix(k2ltheta) * rzMatrix(k2llambda); + k1r = k1r * rzMatrix(k2lphi); + k2r = ryMatrix(k2rtheta) * rzMatrix(k2rlambda); + } else if (newSpecialization == Specialization::FSimaabEquiv) { + // :math:`U \sim U_d(\alpha, \alpha, \beta), \alpha \geq |\beta|` + // + // This gate binds 5 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l)\cdot Rz(\lambda_l)`. + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, EulerBasis::ZYZ); + auto ab = (a + b) / 2.; + + a = ab; + b = ab; + // unmodified parameter c + globalPhase = globalPhase + k2lphase; + k1l = k1l * rzMatrix(k2lphi); + k2l = ryMatrix(k2ltheta) * rzMatrix(k2llambda); + k1r = k1r * rzMatrix(k2lphi); + k2r = rzMatrix(-k2lphi) * k2r; + } else if (newSpecialization == Specialization::FSimabbEquiv) { + // :math:`U \sim U_d(\alpha, \beta, -\beta), \alpha \geq \beta \geq 0` + // + // This gate binds 5 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l)Rx(\lambda_l)` + auto eulerBasis = EulerBasis::XYX; + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, eulerBasis); + auto bc = (b + c) / 2.; + + // unmodified parameter a + b = bc; + c = bc; + globalPhase = globalPhase + k2lphase; + k1l = k1l * rxMatrix(k2lphi); + k2l = ryMatrix(k2ltheta) * rxMatrix(k2llambda); + k1r = k1r * rxMatrix(k2lphi); + k2r = rxMatrix(-k2lphi) * k2r; + defaultEulerBasis = eulerBasis; + } else if (newSpecialization == Specialization::FSimabmbEquiv) { + // :math:`U \sim U_d(\alpha, \beta, -\beta), \alpha \geq \beta \geq 0` + // + // This gate binds 5 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l)Rx(\lambda_l)` + auto eulerBasis = EulerBasis::XYX; + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, eulerBasis); + auto bc = (b - c) / 2.; + + // unmodified parameter a + b = bc; + c = -bc; + globalPhase = globalPhase + k2lphase; + k1l = k1l * rxMatrix(k2lphi); + k2l = ryMatrix(k2ltheta) * rxMatrix(k2llambda); + k1r = k1r * IPZ * rxMatrix(k2lphi) * IPZ; + k2r = IPZ * rxMatrix(-k2lphi) * IPZ * k2r; + defaultEulerBasis = eulerBasis; + } else { + throw std::logic_error{"Unknown specialization"}; + } + return flippedFromOriginal; + } +}; +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Passes.h b/mlir/include/mlir/Passes/Passes.h new file mode 100644 index 000000000..e60409041 --- /dev/null +++ b/mlir/include/mlir/Passes/Passes.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCODialect.h" + +#include +#include +#include + +namespace mlir { + +class RewritePatternSet; + +} // namespace mlir + +namespace mlir::qco { + +#define GEN_PASS_DECL +#include "mlir/Passes/Passes.h.inc" // IWYU pragma: export + +void populateGateDecompositionPatterns(mlir::RewritePatternSet& patterns); + +//===----------------------------------------------------------------------===// +// Registration +//===----------------------------------------------------------------------===// + +/// Generate the code for registering passes. +#define GEN_PASS_REGISTRATION +#include "mlir/Passes/Passes.h.inc" // IWYU pragma: export + +} // namespace mlir::qco diff --git a/mlir/include/mlir/Passes/Passes.td b/mlir/include/mlir/Passes/Passes.td new file mode 100644 index 000000000..ff071fb83 --- /dev/null +++ b/mlir/include/mlir/Passes/Passes.td @@ -0,0 +1,27 @@ +// Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +// Copyright (c) 2025 Munich Quantum Software Company GmbH +// All rights reserved. +// +// SPDX-License-Identifier: MIT +// +// Licensed under the MIT License + +#ifndef QCO_PASSES +#define QCO_PASSES + +include "mlir/Pass/PassBase.td" + +//===----------------------------------------------------------------------===// +// Optimization Passes +//===----------------------------------------------------------------------===// + +def GateDecompositionPass : Pass<"gate-decomposition", "mlir::ModuleOp"> { + let dependentDialects = [ "mlir::arith::ArithDialect", "mlir::qco::QCODialect" ]; + let summary = "This pass performs various gate decompositions to translate quantum gates being used."; + let description = [{ + Decomposes series of operations that operate on up to two qubits into a sequence of up to three + two-qubit basis gates and single-qubit operations. + }]; +} + +#endif // QCO_PASSES diff --git a/mlir/lib/CMakeLists.txt b/mlir/lib/CMakeLists.txt index 8b2ef74e7..b4a19a14d 100644 --- a/mlir/lib/CMakeLists.txt +++ b/mlir/lib/CMakeLists.txt @@ -6,7 +6,8 @@ # # Licensed under the MIT License -add_subdirectory(Dialect) add_subdirectory(Conversion) add_subdirectory(Compiler) +add_subdirectory(Dialect) +add_subdirectory(Passes) add_subdirectory(Support) diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 1e4483754..7096a2554 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -21,7 +21,8 @@ add_mlir_library( QCOToQC QCToQIR MQT::MLIRSupport - MQT::ProjectOptions) + MQT::ProjectOptions + QcoPasses) # collect header files file(GLOB_RECURSE COMPILER_HEADERS_SOURCE "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/*.h") diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index d8cbd3b8b..53a8b38d7 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -13,6 +13,7 @@ #include "mlir/Conversion/QCOToQC/QCOToQC.h" #include "mlir/Conversion/QCToQCO/QCToQCO.h" #include "mlir/Conversion/QCToQIR/QCToQIR.h" +#include "mlir/Passes/Passes.h" #include "mlir/Support/PrettyPrinting.h" #include @@ -64,6 +65,11 @@ void QuantumCompilerPipeline::addCleanupPasses(PassManager& pm) { pm.addPass(createRemoveDeadValuesPass()); } +void QuantumCompilerPipeline::addOptimizationPasses(PassManager& pm) { + // Always run all optimization passes for now + pm.addPass(qco::createGateDecompositionPass()); +} + void QuantumCompilerPipeline::configurePassManager(PassManager& pm) const { // Enable timing statistics if requested if (config_.enableTiming) { @@ -160,7 +166,7 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, pm.clear(); // Stage 5: Optimization passes - // TODO: Add optimization passes + addOptimizationPasses(pm); addCleanupPasses(pm); if (failed(pm.run(module))) { return failure(); diff --git a/mlir/lib/Dialect/QCO/IR/CMakeLists.txt b/mlir/lib/Dialect/QCO/IR/CMakeLists.txt index 567a80611..028d3520b 100644 --- a/mlir/lib/Dialect/QCO/IR/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/IR/CMakeLists.txt @@ -24,7 +24,8 @@ add_mlir_dialect_library( LINK_LIBS PUBLIC MLIRIR - MLIRSideEffectInterfaces) + MLIRSideEffectInterfaces + Eigen3::Eigen) # collect header files file(GLOB_RECURSE IR_HEADERS_SOURCE "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/IR/*.h") diff --git a/mlir/lib/Passes/CMakeLists.txt b/mlir/lib/Passes/CMakeLists.txt new file mode 100644 index 000000000..8632a3cca --- /dev/null +++ b/mlir/lib/Passes/CMakeLists.txt @@ -0,0 +1,42 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) +set(LIBRARIES ${dialect_libs} MQT::CoreIR Eigen3::Eigen) +add_compile_options(-fexceptions) + +file(GLOB_RECURSE PASSES_SOURCES *.cpp) + +add_mlir_library( + QcoPasses + ${PASSES_SOURCES} + LINK_LIBS + PUBLIC + ${LIBRARIES} + DEPENDS + QcoPassesIncGen) + +# collect header files +file(GLOB_RECURSE PASSES_HEADERS_SOURCE ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Passes/*.h) +file(GLOB_RECURSE PASSES_HEADERS_BUILD ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Passes/*.inc) + +# add public headers using file sets +target_sources( + QcoPasses + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_SOURCE_INCLUDE_DIR} + FILES + ${PASSES_HEADERS_SOURCE} + FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_BUILD_INCLUDE_DIR} + FILES + ${PASSES_HEADERS_BUILD}) diff --git a/mlir/lib/Passes/GateDecompositionPass.cpp b/mlir/lib/Passes/GateDecompositionPass.cpp new file mode 100644 index 000000000..88198f434 --- /dev/null +++ b/mlir/lib/Passes/GateDecompositionPass.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Passes/Passes.h" + +#include +#include +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_GATEDECOMPOSITIONPASS +#include "mlir/Passes/Passes.h.inc" + +/** + * @brief This pass attempts to collect as many operations as possible into a + * 4x4 unitary matrix and then decompose it into 1q rotations and 2q + * basis gates. + */ +struct GateDecompositionPass final + : impl::GateDecompositionPassBase { + + void runOnOperation() override { + // Get the current operation being operated on. + auto op = getOperation(); + auto* ctx = &getContext(); + + // Define the set of patterns to use. + mlir::RewritePatternSet patterns(ctx); + populateGateDecompositionPatterns(patterns); + + // Configure greedy driver + mlir::GreedyRewriteConfig config; + config.setUseTopDownTraversal(true); + + // Apply patterns in an iterative and greedy manner. + if (mlir::failed( + mlir::applyPatternsGreedily(op, std::move(patterns), config))) { + signalPassFailure(); + } + } +}; + +} // namespace mlir::qco diff --git a/mlir/lib/Passes/Patterns/GateDecompositionPattern.cpp b/mlir/lib/Passes/Patterns/GateDecompositionPattern.cpp new file mode 100644 index 000000000..662991b68 --- /dev/null +++ b/mlir/lib/Passes/Patterns/GateDecompositionPattern.cpp @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Passes/Decomposition/BasisDecomposer.h" +#include "mlir/Passes/Decomposition/EulerBasis.h" +#include "mlir/Passes/Decomposition/EulerDecomposition.h" +#include "mlir/Passes/Decomposition/Gate.h" +#include "mlir/Passes/Decomposition/GateSequence.h" +#include "mlir/Passes/Decomposition/Helpers.h" +#include "mlir/Passes/Decomposition/UnitaryMatrices.h" +#include "mlir/Passes/Decomposition/WeylDecomposition.h" +#include "mlir/Passes/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +/** + * @brief This pattern attempts to collect as many operations as possible into a + * 4x4 unitary matrix and then decompose it into rotation and given basis + * gates. + */ +struct GateDecompositionPattern final + : mlir::OpInterfaceRewritePattern { + using EulerBasis = decomposition::EulerBasis; + using Gate = decomposition::Gate; + + /** + * Initialize pattern with a set of basis gates and euler bases. + * The best combination of (basis gate, euler basis) will be evaluated for + * each decomposition. + */ + explicit GateDecompositionPattern(mlir::MLIRContext* context, + llvm::SmallVector basisGate, + llvm::SmallVector eulerBasis) + : OpInterfaceRewritePattern(context), + decomposerBasisGate{std::move(basisGate)}, + decomposerEulerBases{std::move(eulerBasis)} { + for (auto&& basisGate : decomposerBasisGate) { + basisDecomposers.push_back(decomposition::TwoQubitBasisDecomposer::create( + basisGate, DEFAULT_FIDELITY)); + } + } + + mlir::LogicalResult + matchAndRewrite(UnitaryOpInterface op, + mlir::PatternRewriter& rewriter) const override { + auto series = TwoQubitSeries::getTwoQubitSeries(op); + + if (series.gates.size() < 3) { + // too short + return mlir::failure(); + } + + std::optional bestSequence; + + if (series.isSingleQubitSeries()) { + // only a single-qubit series; + // single-qubit euler decomposition is more efficient + const matrix2x2 unitaryMatrix = series.getSingleQubitUnitaryMatrix(); + for (auto&& eulerBasis : decomposerEulerBases) { + auto sequence = decomposition::EulerDecomposition::generateCircuit( + eulerBasis, unitaryMatrix, true, std::nullopt); + if (!bestSequence || + sequence.complexity() < bestSequence->complexity()) { + bestSequence = sequence; + } + } + } else { + // two-qubit series; perform two-qubit basis decomposition + const matrix4x4 unitaryMatrix = series.getUnitaryMatrix(); + const auto targetDecomposition = + decomposition::TwoQubitWeylDecomposition::create(unitaryMatrix, + DEFAULT_FIDELITY); + + for (const auto& decomposer : basisDecomposers) { + auto sequence = decomposer.twoQubitDecompose( + targetDecomposition, decomposerEulerBases, DEFAULT_FIDELITY, false, + std::nullopt); + if (sequence) { + if (!bestSequence || + sequence->complexity() < bestSequence->complexity()) { + bestSequence = sequence; + } + } + } + } + + llvm::errs() << "Found series (" << series.complexity << "): "; + for (auto&& gate : series.gates) { + llvm::errs() << gate.op->getName().stripDialect().str() << ", "; + } + + if (!bestSequence) { + return mlir::failure(); + } + llvm::errs() << "\nDecomposition (" << bestSequence->complexity() << "): "; + for (auto&& gate : bestSequence->gates) { + llvm::errs() << qc::toString(gate.type) << ", "; + } + llvm::errs() << "\n"; + // only accept new sequence if it shortens existing series by more than two + // gates; this prevents an oscillation with phase gates + if (bestSequence->complexity() + 2 >= series.complexity) { + return mlir::failure(); + } + + applySeries(rewriter, series, *bestSequence); + + return mlir::success(); + } + +protected: + /** + * Factor by which two matrices are considered to be the same when simplifying + * during a decomposition. + */ + static constexpr auto DEFAULT_FIDELITY = 1.0 - 1e-15; + static constexpr auto SANITY_CHECK_PRECISION = + decomposition::SANITY_CHECK_PRECISION; + + using QubitId = decomposition::QubitId; + struct TwoQubitSeries { + /** + * Complexity of series using getComplexity() for each gate. + */ + std::size_t complexity{0}; + /** + * Qubits that are the input for the series. + * First qubit will always be set, second qubit may be equal to + * mlir::Value{} if the series consists of only single-qubit gates. + * + * All + */ + std::array inQubits{}; + /** + * Qubits that are the input for the series. + * First qubit will always be set, second qubit may be equal to + * mlir::Value{} if the series consists of only single-qubit gates. + */ + std::array outQubits{}; + + struct MlirGate { + UnitaryOpInterface op; + llvm::SmallVector qubitIds; + }; + llvm::SmallVector gates; + + [[nodiscard]] static TwoQubitSeries + getTwoQubitSeries(UnitaryOpInterface op) { + if (isBarrier(op)) { + return {}; + } + TwoQubitSeries result(op); + + auto getUser = [](mlir::Value qubit, + auto&& filter) -> std::optional { + if (qubit) { + auto users = qubit.getUsers(); + auto userIt = users.begin(); + // qubit may have more than one use if it is in a ctrl block (one use + // for gate, one use for ctrl); we want to use the ctrl operation + // since it is relevant for the total unitary matrix of the circuit + assert(qubit.hasOneUse() || qubit.hasNUses(2)); + if (!qubit.hasOneUse()) { + // TODO: use wire iterator for proper handling + while (!mlir::dyn_cast(*userIt)) { + ++userIt; + if (userIt == users.end()) { + // TODO: should not happen? + return std::nullopt; + } + } + } + auto user = mlir::dyn_cast(*userIt); + if (user && filter(user)) { + return user; + } + } + return std::nullopt; + }; + + bool foundGate = true; + while (foundGate) { + foundGate = false; + // collect all available single-qubit operations + for (std::size_t i = 0; i < result.outQubits.size(); ++i) { + while (auto user = getUser(result.outQubits[i], + &helpers::isSingleQubitOperation)) { + foundGate = result.appendSingleQubitGate(*user); + } + } + + for (std::size_t i = 0; i < result.outQubits.size(); ++i) { + if (auto user = + getUser(result.outQubits[i], &helpers::isTwoQubitOperation)) { + foundGate = result.appendTwoQubitGate(*user); + break; // go back to single-qubit collection + } + } + } + return result; + } + + [[nodiscard]] matrix2x2 getSingleQubitUnitaryMatrix() { + auto unitaryMatrix = decomposition::IDENTITY_GATE; + for (auto&& gate : gates) { + // auto gateMatrix = gate.op.getFastUnitaryMatrix(); + auto gateMatrix = gate.op.getUnitaryMatrix(); + unitaryMatrix = gateMatrix * unitaryMatrix; + } + + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; + } + + [[nodiscard]] matrix4x4 getUnitaryMatrix() { + matrix4x4 unitaryMatrix = helpers::kroneckerProduct( + decomposition::IDENTITY_GATE, decomposition::IDENTITY_GATE); + for (auto&& gate : gates) { + auto gateMatrix = gate.op.getUnitaryMatrix(); + if (gate.op.isSingleQubit()) { + assert(gate.qubitIds.size() == 1); + // TODO: use helpers::kroneckerProduct or Eigen::kroneckerProduct? + if (gate.qubitIds[0] == 0) { + gateMatrix = Eigen::kroneckerProduct(decomposition::IDENTITY_GATE, + gateMatrix) + .eval(); + } else if (gate.qubitIds[0] == 1) { + gateMatrix = Eigen::kroneckerProduct(gateMatrix, + decomposition::IDENTITY_GATE) + .eval(); + } + } + // auto gateMatrix = gate.op.getFastUnitaryMatrix(); + unitaryMatrix = gateMatrix * unitaryMatrix; + } + + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; + } + + [[nodiscard]] bool isSingleQubitSeries() const { + return llvm::is_contained(inQubits, mlir::Value{}) || + llvm::is_contained(outQubits, mlir::Value{}); + } + + private: + /** + * Initialize empty TwoQubitSeries instance. + * New operations can *NOT* be added when calling this constructor overload. + */ + TwoQubitSeries() = default; + /** + * Initialize TwoQubitSeries instance with given first operation. + */ + explicit TwoQubitSeries(UnitaryOpInterface initialOperation) { + if (helpers::isSingleQubitOperation(initialOperation)) { + inQubits = {initialOperation.getInputQubit(0), mlir::Value{}}; + outQubits = {initialOperation.getOutputQubit(0), mlir::Value{}}; + gates.push_back({.op = initialOperation, .qubitIds = {0}}); + } else if (helpers::isTwoQubitOperation(initialOperation)) { + inQubits = {initialOperation.getInputQubit(0), + initialOperation.getInputQubit(1)}; + outQubits = {initialOperation.getOutputQubit(0), + initialOperation.getOutputQubit(1)}; + gates.push_back({.op = initialOperation, .qubitIds = {0, 1}}); + } + complexity += helpers::getComplexity(helpers::getQcType(initialOperation), + initialOperation.getNumQubits()); + } + + /** + * @return true if series continues, otherwise false + * (will always return true) + */ + bool appendSingleQubitGate(UnitaryOpInterface nextGate) { + if (isBarrier(nextGate)) { + return false; + } + auto operand = nextGate.getInputQubit(0); + // NOLINTNEXTLINE(readability-qualified-auto) + auto it = llvm::find(outQubits, operand); + if (it == outQubits.end()) { + throw std::logic_error{"Operand of single-qubit op and user of " + "qubit is not in current outQubits"}; + } + QubitId qubitId = std::distance(outQubits.begin(), it); + *it = nextGate->getResult(0); + + gates.push_back({.op = nextGate, .qubitIds = {qubitId}}); + complexity += helpers::getComplexity(helpers::getQcType(nextGate), 1); + return true; + } + + /** + * @return true if series continues, otherwise false + */ + bool appendTwoQubitGate(UnitaryOpInterface nextGate) { + auto&& firstOperand = nextGate.getInputQubit(0); + auto&& secondOperand = nextGate.getInputQubit(1); + auto firstQubitIt = // NOLINT(readability-qualified-auto) + llvm::find(outQubits, firstOperand); + auto secondQubitIt = // NOLINT(readability-qualified-auto) + llvm::find(outQubits, secondOperand); + if (firstQubitIt == outQubits.end() || secondQubitIt == outQubits.end()) { + // another qubit is involved, series is finished (except there only + // has been one qubit so far) + auto it = // NOLINT(readability-qualified-auto) + llvm::find(outQubits, mlir::Value{}); + if (it == outQubits.end()) { + return false; + } + // TODO: this only works because parameters are at end of operands; + // use future getInputQubits() instead + auto&& opInQubits = nextGate->getOperands(); + // iterator in the operation input of "old" qubit that already has + // previous single-qubit gates in this series + auto it2 = llvm::find(opInQubits, firstQubitIt != outQubits.end() + ? *firstQubitIt + : *secondQubitIt); + // new qubit ID based on position in outQubits + const QubitId newInQubitId = std::distance(outQubits.begin(), it); + // position in operation input; since there are only two qubits, it must + // be the "not old" one + const QubitId newOpInQubitId = + 1 - std::distance(opInQubits.begin(), it2); + + // update inQubit and update dangling iterator, then proceed as usual + inQubits[newInQubitId] = opInQubits[newOpInQubitId]; + firstQubitIt = (firstQubitIt != outQubits.end()) ? firstQubitIt : it; + secondQubitIt = (secondQubitIt != outQubits.end()) ? secondQubitIt : it; + + // before proceeding as usual, see if backtracking on the "new" qubit is + // possible to collect other single-qubit operations + backtrackSingleQubitSeries(newInQubitId); + } + if (isBarrier(nextGate)) { + // a barrier operation should not be crossed for a decomposition + return false; + } + const QubitId firstQubitId = + std::distance(outQubits.begin(), firstQubitIt); + const QubitId secondQubitId = + std::distance(outQubits.begin(), secondQubitIt); + *firstQubitIt = nextGate->getResult(0); + *secondQubitIt = nextGate->getResult(1); + + gates.push_back( + {.op = nextGate, .qubitIds = {firstQubitId, secondQubitId}}); + complexity += helpers::getComplexity(helpers::getQcType(nextGate), 2); + return true; + } + + /** + * Traverse single-qubit series back from a given qubit. + * This is used when a series starts with single-qubit gates and then + * encounters a two-qubit gate. The second qubit involved in the two-qubit + * gate could have previous single-qubit operations that can be incorporated + * in the series. + */ + void backtrackSingleQubitSeries(QubitId qubitId) { + auto prependSingleQubitGate = [&](UnitaryOpInterface op) { + inQubits[qubitId] = op.getInputQubit(0); + gates.insert(gates.begin(), {.op = op, .qubitIds = {qubitId}}); + // outQubits do not need to be updated because the final out qubit is + // already fixed + }; + while (auto* op = inQubits[qubitId].getDefiningOp()) { + auto unitaryOp = mlir::dyn_cast(op); + if (unitaryOp && helpers::isSingleQubitOperation(unitaryOp) && + !isBarrier(unitaryOp)) { + prependSingleQubitGate(unitaryOp); + } else { + break; + } + } + } + + [[nodiscard]] static bool isBarrier(UnitaryOpInterface op) { + return llvm::isa_and_nonnull(*op); + } + }; + + template + static OpType createGate(mlir::PatternRewriter& rewriter, + mlir::Location location, + Args&&... inQubitsAndParams) { + return rewriter.create(location, + std::forward(inQubitsAndParams)...); + } + + template + static CtrlOp createControlledGate(mlir::PatternRewriter& rewriter, + mlir::Location location, + mlir::ValueRange ctrlQubits, + Args&&... inQubitsAndParams) { + llvm::SmallVector inQubits; + auto collectInQubits = [&inQubits](auto&& x) { + if constexpr (std::is_same_v, + mlir::Value>) { + // if argument is a qubit, add it to list; otherwise, do nothing + inQubits.push_back(std::forward(x)); + } + }; + (collectInQubits(inQubitsAndParams), ...); + return rewriter.create( + location, ctrlQubits, mlir::ValueRange{inQubits}, + createGate(rewriter, location, + std::forward(inQubitsAndParams)...)); + } + + static void applySeries(mlir::PatternRewriter& rewriter, + TwoQubitSeries& series, + const decomposition::TwoQubitGateSequence& sequence) { + auto& lastSeriesOp = series.gates.back().op; + auto location = lastSeriesOp->getLoc(); + rewriter.setInsertionPointAfter(lastSeriesOp); + + auto inQubits = series.inQubits; + auto updateInQubits = + [&inQubits](const llvm::SmallVector& qubitIds, + auto&& newGate) { + if (qubitIds.size() == 2) { + inQubits[qubitIds[0]] = newGate.getOutputQubit(0); + inQubits[qubitIds[1]] = newGate.getOutputQubit(1); + } else if (qubitIds.size() == 1) { + inQubits[qubitIds[0]] = newGate.getOutputQubit(0); + } else { + throw std::logic_error{"Invalid number of qubit IDs!"}; + } + }; + + if (sequence.hasGlobalPhase()) { + createGate(rewriter, location, sequence.globalPhase); + } + + matrix4x4 unitaryMatrix = helpers::kroneckerProduct( + decomposition::IDENTITY_GATE, decomposition::IDENTITY_GATE); + for (auto&& gate : sequence.gates) { + auto gateMatrix = decomposition::getTwoQubitMatrix(gate); + unitaryMatrix = gateMatrix * unitaryMatrix; + + // TODO: need to add each basis gate we want to use + if (gate.type == qc::X) { + mlir::SmallVector inCtrlQubits; + if (gate.qubitId.size() > 1) { + // controls come last + inCtrlQubits.push_back(inQubits[gate.qubitId[1]]); + } + auto newGate = createControlledGate( + rewriter, location, inCtrlQubits, inQubits[gate.qubitId[0]]); + updateInQubits(gate.qubitId, newGate); + } else if (gate.type == qc::RX) { + assert(gate.qubitId.size() == 1); + auto newGate = createGate( + rewriter, location, inQubits[gate.qubitId[0]], gate.parameter[0]); + updateInQubits(gate.qubitId, newGate); + } else if (gate.type == qc::RY) { + assert(gate.qubitId.size() == 1); + auto newGate = createGate( + rewriter, location, inQubits[gate.qubitId[0]], gate.parameter[0]); + updateInQubits(gate.qubitId, newGate); + } else if (gate.type == qc::RZ) { + assert(gate.qubitId.size() == 1); + auto newGate = createGate( + rewriter, location, inQubits[gate.qubitId[0]], gate.parameter[0]); + updateInQubits(gate.qubitId, newGate); + } else { + throw std::runtime_error{"Unknown gate type!"}; + } + } + assert((unitaryMatrix * std::exp(IM * sequence.globalPhase)) + .isApprox(series.getUnitaryMatrix(), SANITY_CHECK_PRECISION)); + + if (series.isSingleQubitSeries()) { + rewriter.replaceAllUsesWith(series.outQubits[0], inQubits[0]); + } else { + rewriter.replaceAllUsesWith(series.outQubits, inQubits); + } + for (auto&& gate : llvm::reverse(series.gates)) { + if (auto ctrlOp = llvm::dyn_cast(*gate.op)) { + rewriter.eraseBlock(&ctrlOp.getBody().front()); + } + rewriter.eraseOp(gate.op); + } + } + +private: + llvm::SmallVector decomposerBasisGate; + llvm::SmallVector basisDecomposers; + llvm::SmallVector decomposerEulerBases; +}; + +/** + * @brief Populates the given pattern set with patterns for gate + * decomposition. + */ +void populateGateDecompositionPatterns(mlir::RewritePatternSet& patterns) { + llvm::SmallVector basisGates; + llvm::SmallVector eulerBases; + basisGates.push_back({.type = qc::X, .parameter = {}, .qubitId = {0, 1}}); + basisGates.push_back({.type = qc::X, .parameter = {}, .qubitId = {1, 0}}); + eulerBases.push_back(GateDecompositionPattern::EulerBasis::ZYZ); + eulerBases.push_back(GateDecompositionPattern::EulerBasis::XYX); + eulerBases.push_back(GateDecompositionPattern::EulerBasis::ZXZ); + patterns.add(patterns.getContext(), basisGates, + eulerBases); +} + +} // namespace mlir::qco diff --git a/mlir/unittests/CMakeLists.txt b/mlir/unittests/CMakeLists.txt index 43ffbbd24..32a1a44fa 100644 --- a/mlir/unittests/CMakeLists.txt +++ b/mlir/unittests/CMakeLists.txt @@ -6,11 +6,13 @@ # # Licensed under the MIT License +add_subdirectory(decomposition) add_subdirectory(dialect) add_subdirectory(pipeline) add_subdirectory(translation) add_custom_target(mqt-core-mlir-unittests) -add_dependencies(mqt-core-mlir-unittests mqt-core-mlir-compiler-pipeline-test - mqt-core-mlir-translation-test mqt-core-mlir-wireiterator-test) +add_dependencies( + mqt-core-mlir-unittests mqt-core-mlir-decomposition-test mqt-core-mlir-compiler-pipeline-test + mqt-core-mlir-translation-test mqt-core-mlir-wireiterator-test) diff --git a/mlir/unittests/decomposition/CMakeLists.txt b/mlir/unittests/decomposition/CMakeLists.txt new file mode 100644 index 000000000..f845b432a --- /dev/null +++ b/mlir/unittests/decomposition/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(testname "mqt-core-mlir-decomposition-test") +file(GLOB_RECURSE DECOMPOSITION_TEST_SOURCES *.cpp) + +if(NOT TARGET ${testname}) + # create an executable in which the tests will be stored + add_executable(${testname} ${DECOMPOSITION_TEST_SOURCES}) + # link the Google test infrastructure and a default main function to the test executable. + target_link_libraries( + ${testname} + PRIVATE GTest::gmock + GTest::gtest_main + LLVMFileCheck + MLIRPass + MLIRTransforms + MQTCompilerPipeline + MQT::CoreIR + Eigen3::Eigen) + # discover tests + gtest_discover_tests(${testname} DISCOVERY_TIMEOUT 60) + set_target_properties(${testname} PROPERTIES FOLDER unittests) +endif() diff --git a/mlir/unittests/decomposition/test_basis_decomposer.cpp b/mlir/unittests/decomposition/test_basis_decomposer.cpp new file mode 100644 index 000000000..7f9221f5a --- /dev/null +++ b/mlir/unittests/decomposition/test_basis_decomposer.cpp @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ir/operations/OpType.hpp" +#include "mlir/Passes/Decomposition/BasisDecomposer.h" +#include "mlir/Passes/Decomposition/EulerBasis.h" +#include "mlir/Passes/Decomposition/Gate.h" +#include "mlir/Passes/Decomposition/GateSequence.h" +#include "mlir/Passes/Decomposition/Helpers.h" +#include "mlir/Passes/Decomposition/UnitaryMatrices.h" +#include "mlir/Passes/Decomposition/WeylDecomposition.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +namespace { +[[nodiscard]] matrix4x4 randomUnitaryMatrix() { + [[maybe_unused]] static auto initializeRandom = []() { + // Eigen uses std::rand() internally, use fixed seed for deterministic + // testing behavior + std::srand(123456UL); + return true; + }(); + const matrix4x4 randomMatrix = matrix4x4::Random(); + Eigen::HouseholderQR qr{}; // NOLINT(misc-include-cleaner) + qr.compute(randomMatrix); + const matrix4x4 unitaryMatrix = qr.householderQ(); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} + +[[nodiscard]] matrix4x4 canonicalGate(fp a, fp b, fp c) { + TwoQubitWeylDecomposition tmp{}; + tmp.a = a; + tmp.b = b; + tmp.c = c; + return tmp.getCanonicalMatrix(); +} +} // namespace + +class BasisDecomposerTest + : public testing::TestWithParam< + std::tuple, matrix4x4>> { +public: + void SetUp() override { + basisGate = std::get<0>(GetParam()); + eulerBases = std::get<1>(GetParam()); + target = std::get<2>(GetParam()); + targetDecomposition = TwoQubitWeylDecomposition::create(target, 1.0); + } + + [[nodiscard]] static matrix4x4 restore(const TwoQubitGateSequence& sequence) { + matrix4x4 matrix = matrix4x4::Identity(); + for (auto&& gate : sequence.gates) { + matrix = getTwoQubitMatrix(gate) * matrix; + } + + matrix *= std::exp(IM * sequence.globalPhase); + return matrix; + } + +protected: + matrix4x4 target; + Gate basisGate; + llvm::SmallVector eulerBases; + TwoQubitWeylDecomposition targetDecomposition; +}; + +TEST_P(BasisDecomposerTest, TestExact) { + const auto& originalMatrix = target; + auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0); + auto decomposedSequence = decomposer.twoQubitDecompose( + targetDecomposition, eulerBases, 1.0, false, std::nullopt); + + ASSERT_TRUE(decomposedSequence.has_value()); + + auto restoredMatrix = restore(*decomposedSequence); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST_P(BasisDecomposerTest, TestApproximation) { + const auto& originalMatrix = target; + auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0 - 1e-12); + auto decomposedSequence = decomposer.twoQubitDecompose( + targetDecomposition, eulerBases, 1.0 - 1e-12, true, std::nullopt); + + ASSERT_TRUE(decomposedSequence.has_value()); + + auto restoredMatrix = restore(*decomposedSequence); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST(BasisDecomposerTest, Random) { + auto stopTime = std::chrono::steady_clock::now() + std::chrono::seconds{10}; + auto iterations = 0; + + const Gate basisGate{.type = qc::X, .parameter = {}, .qubitId = {0, 1}}; + const llvm::SmallVector eulerBases = {EulerBasis::XYX, + EulerBasis::ZXZ}; + + while (std::chrono::steady_clock::now() < stopTime) { + auto originalMatrix = randomUnitaryMatrix(); + + auto targetDecomposition = + TwoQubitWeylDecomposition::create(originalMatrix, 1.0); + auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0); + auto decomposedSequence = decomposer.twoQubitDecompose( + targetDecomposition, eulerBases, 1.0, true, std::nullopt); + + ASSERT_TRUE(decomposedSequence.has_value()); + + auto restoredMatrix = BasisDecomposerTest::restore(*decomposedSequence); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "ORIGINAL:\n" + << originalMatrix << '\n' + << "RESULT:\n" + << restoredMatrix << '\n'; + ++iterations; + } + + RecordProperty("iterations", iterations); + std::cerr << "Iterations: " << iterations << '\n'; +} + +INSTANTIATE_TEST_CASE_P( + SingleQubitMatrices, BasisDecomposerTest, + testing::Combine( + // basis gates + testing::Values(Gate{.type = qc::X, .parameter = {}, .qubitId = {0, 1}}, + Gate{ + .type = qc::X, .parameter = {}, .qubitId = {1, 0}}), + // sets of euler bases + testing::Values(llvm::SmallVector{EulerBasis::ZYZ}, + llvm::SmallVector{ + EulerBasis::ZYZ, EulerBasis::ZXZ, EulerBasis::XYX, + EulerBasis::XZX}, + llvm::SmallVector{EulerBasis::XZX}), + // targets to be decomposed + testing::Values(helpers::kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE), + helpers::kroneckerProduct(rzMatrix(1.0), ryMatrix(3.1)), + helpers::kroneckerProduct(IDENTITY_GATE, + rxMatrix(0.1))))); + +INSTANTIATE_TEST_CASE_P( + TwoQubitMatrices, BasisDecomposerTest, + testing::Combine( + // basis gates + testing::Values(Gate{.type = qc::X, .parameter = {}, .qubitId = {0, 1}}, + Gate{ + .type = qc::X, .parameter = {}, .qubitId = {1, 0}}), + // sets of euler bases + testing::Values(llvm::SmallVector{EulerBasis::ZYZ}, + llvm::SmallVector{ + EulerBasis::ZYZ, EulerBasis::ZXZ, EulerBasis::XYX, + EulerBasis::XZX}, + llvm::SmallVector{EulerBasis::XZX}), + // targets to be decomposed + ::testing::Values( + rzzMatrix(2.0), ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0), + canonicalGate(1.5, -0.2, 0.0) * + helpers::kroneckerProduct(rxMatrix(1.0), IDENTITY_GATE), + helpers::kroneckerProduct(rxMatrix(1.0), ryMatrix(1.0)) * + canonicalGate(1.1, 0.2, 3.0) * + helpers::kroneckerProduct(rxMatrix(1.0), IDENTITY_GATE), + helpers::kroneckerProduct(H_GATE, IPZ) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + helpers::kroneckerProduct(IPX, IPY)))); diff --git a/mlir/unittests/decomposition/test_euler_decomposition.cpp b/mlir/unittests/decomposition/test_euler_decomposition.cpp new file mode 100644 index 000000000..442bbb9c3 --- /dev/null +++ b/mlir/unittests/decomposition/test_euler_decomposition.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Passes/Decomposition/EulerBasis.h" +#include "mlir/Passes/Decomposition/EulerDecomposition.h" +#include "mlir/Passes/Decomposition/GateSequence.h" +#include "mlir/Passes/Decomposition/Helpers.h" +#include "mlir/Passes/Decomposition/UnitaryMatrices.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +namespace { +[[nodiscard]] matrix2x2 randomUnitaryMatrix() { + [[maybe_unused]] static auto initializeRandom = []() { + // Eigen uses std::rand() internally, use fixed seed for deterministic + // testing behavior + std::srand(123456UL); + return true; + }(); + const matrix2x2 randomMatrix = matrix2x2::Random(); + Eigen::HouseholderQR qr{}; // NOLINT(misc-include-cleaner) + qr.compute(randomMatrix); + const matrix2x2 unitaryMatrix = qr.householderQ(); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} +} // namespace + +class EulerDecompositionTest + : public testing::TestWithParam> { +public: + [[nodiscard]] static matrix2x2 restore(const TwoQubitGateSequence& sequence) { + matrix2x2 matrix = matrix2x2::Identity(); + for (auto&& gate : sequence.gates) { + matrix = getSingleQubitMatrix(gate) * matrix; + } + + matrix *= std::exp(IM * sequence.globalPhase); + return matrix; + } + + void SetUp() override { + eulerBasis = std::get<0>(GetParam()); + originalMatrix = std::get<1>(GetParam()); + } + +protected: + matrix2x2 originalMatrix; + EulerBasis eulerBasis{}; +}; + +TEST_P(EulerDecompositionTest, TestExact) { + auto decomposition = EulerDecomposition::generateCircuit( + eulerBasis, originalMatrix, false, 0.0); + auto restoredMatrix = restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST(EulerDecompositionTest, Random) { + auto stopTime = std::chrono::steady_clock::now() + std::chrono::seconds{10}; + auto iterations = 0; + auto eulerBases = std::array{EulerBasis::XYX, EulerBasis::XZX, + EulerBasis::ZYZ, EulerBasis::ZXZ}; + std::size_t currentEulerBase = 0; + while (std::chrono::steady_clock::now() < stopTime) { + auto originalMatrix = randomUnitaryMatrix(); + auto eulerBasis = eulerBases[currentEulerBase++ % eulerBases.size()]; + auto decomposition = EulerDecomposition::generateCircuit( + eulerBasis, originalMatrix, true, std::nullopt); + auto restoredMatrix = EulerDecompositionTest::restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "ORIGINAL:\n" + << originalMatrix << '\n' + << "RESULT:\n" + << restoredMatrix << '\n'; + ++iterations; + } + + RecordProperty("iterations", iterations); + std::cerr << "Iterations: " << iterations << '\n'; +} + +INSTANTIATE_TEST_CASE_P( + SingleQubitMatrices, EulerDecompositionTest, + testing::Combine(testing::Values(EulerBasis::XYX, EulerBasis::XZX, + EulerBasis::ZYZ, EulerBasis::ZXZ), + testing::Values(IDENTITY_GATE, ryMatrix(2.0), + rxMatrix(0.5), rzMatrix(3.14), H_GATE))); diff --git a/mlir/unittests/decomposition/test_weyl_decomposition.cpp b/mlir/unittests/decomposition/test_weyl_decomposition.cpp new file mode 100644 index 000000000..9b205abe2 --- /dev/null +++ b/mlir/unittests/decomposition/test_weyl_decomposition.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ir/operations/OpType.hpp" +#include "mlir/Passes/Decomposition/Helpers.h" +#include "mlir/Passes/Decomposition/UnitaryMatrices.h" +#include "mlir/Passes/Decomposition/WeylDecomposition.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +namespace { +[[nodiscard]] matrix4x4 randomUnitaryMatrix() { + [[maybe_unused]] static auto initializeRandom = []() { + // Eigen uses std::rand() internally, use fixed seed for deterministic + // testing behavior + std::srand(123456UL); + return true; + }(); + const matrix4x4 randomMatrix = matrix4x4::Random(); + Eigen::HouseholderQR qr{}; // NOLINT(misc-include-cleaner) + qr.compute(randomMatrix); + const matrix4x4 unitaryMatrix = qr.householderQ(); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} + +[[nodiscard]] matrix4x4 canonicalGate(fp a, fp b, fp c) { + TwoQubitWeylDecomposition tmp{}; + tmp.a = a; + tmp.b = b; + tmp.c = c; + return tmp.getCanonicalMatrix(); +} +} // namespace + +class WeylDecompositionTest : public testing::TestWithParam { +public: + [[nodiscard]] static matrix4x4 + restore(const TwoQubitWeylDecomposition& decomposition) { + return k1(decomposition) * can(decomposition) * k2(decomposition) * + globalPhaseFactor(decomposition); + } + + [[nodiscard]] static qfp + globalPhaseFactor(const TwoQubitWeylDecomposition& decomposition) { + return std::exp(IM * decomposition.globalPhase); + } + [[nodiscard]] static matrix4x4 + can(const TwoQubitWeylDecomposition& decomposition) { + return decomposition.getCanonicalMatrix(); + } + [[nodiscard]] static matrix4x4 + k1(const TwoQubitWeylDecomposition& decomposition) { + return helpers::kroneckerProduct(decomposition.k1l, decomposition.k1r); + } + [[nodiscard]] static matrix4x4 + k2(const TwoQubitWeylDecomposition& decomposition) { + return helpers::kroneckerProduct(decomposition.k2l, decomposition.k2r); + } +}; + +TEST_P(WeylDecompositionTest, TestExact) { + const auto& originalMatrix = GetParam(); + auto decomposition = TwoQubitWeylDecomposition::create(originalMatrix, 1.0); + auto restoredMatrix = restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST_P(WeylDecompositionTest, TestApproximation) { + const auto& originalMatrix = GetParam(); + auto decomposition = + TwoQubitWeylDecomposition::create(originalMatrix, 1.0 - 1e-12); + auto restoredMatrix = restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST(WeylDecompositionTest, Random) { + auto stopTime = std::chrono::steady_clock::now() + std::chrono::seconds{10}; + auto iterations = 0; + while (std::chrono::steady_clock::now() < stopTime) { + auto originalMatrix = randomUnitaryMatrix(); + auto decomposition = + TwoQubitWeylDecomposition::create(originalMatrix, 1.0 - 1e-12); + auto restoredMatrix = WeylDecompositionTest::restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "ORIGINAL:\n" + << originalMatrix << '\n' + << "RESULT:\n" + << restoredMatrix << '\n'; + ++iterations; + } + + RecordProperty("iterations", iterations); + std::cerr << "Iterations: " << iterations << '\n'; +} + +INSTANTIATE_TEST_CASE_P( + SingleQubitMatrices, WeylDecompositionTest, + ::testing::Values(helpers::kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE), + helpers::kroneckerProduct(rzMatrix(1.0), ryMatrix(3.1)), + helpers::kroneckerProduct(IDENTITY_GATE, rxMatrix(0.1)))); + +INSTANTIATE_TEST_CASE_P( + TwoQubitMatrices, WeylDecompositionTest, + ::testing::Values( + rzzMatrix(2.0), ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0), + canonicalGate(1.5, -0.2, 0.0) * + helpers::kroneckerProduct(rxMatrix(1.0), IDENTITY_GATE), + helpers::kroneckerProduct(rxMatrix(1.0), ryMatrix(1.0)) * + canonicalGate(1.1, 0.2, 3.0) * + helpers::kroneckerProduct(rxMatrix(1.0), IDENTITY_GATE), + helpers::kroneckerProduct(H_GATE, IPZ) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + helpers::kroneckerProduct(IPX, IPY))); + +INSTANTIATE_TEST_CASE_P( + SpecializedMatrices, WeylDecompositionTest, + ::testing::Values( + // id + controlled + general already covered by other parametrizations + // swap equiv + getTwoQubitMatrix({.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {1, 0}}) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}), + // partial swap equiv + canonicalGate(0.5, 0.5, 0.5), + // partial swap equiv (flipped) + canonicalGate(0.5, 0.5, -0.5), + // mirror controlled equiv + getTwoQubitMatrix({.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {1, 0}}), + // sim aab equiv + canonicalGate(0.5, 0.5, 0.1), + // sim abb equiv + canonicalGate(0.5, 0.1, 0.1), + // sim ab-b equiv + canonicalGate(0.5, 0.1, -0.1))); diff --git a/mlir/unittests/pipeline/test_compiler_pipeline.cpp b/mlir/unittests/pipeline/test_compiler_pipeline.cpp index 2b0f1854f..1d8b2ec72 100644 --- a/mlir/unittests/pipeline/test_compiler_pipeline.cpp +++ b/mlir/unittests/pipeline/test_compiler_pipeline.cpp @@ -3725,4 +3725,57 @@ TEST_F(CompilerPipelineTest, Bell) { }); } +TEST_F(CompilerPipelineTest, TwoQubitDecomposition) { + ::qc::QuantumComputation comp; + comp.addQubitRegister(2, "q"); + + auto buildCircuit = [](auto& circuit, auto&& qubitGetter) { + auto constant0 = 2.5; + auto constant1 = 1.2; + auto constant2 = 0.5; + circuit.h(qubitGetter(0)); + circuit.cx(qubitGetter(0), qubitGetter(1)); + circuit.rzz(constant0, qubitGetter(0), qubitGetter(1)); + circuit.ry(constant1, qubitGetter(1)); + circuit.ry(constant1, qubitGetter(0)); + circuit.cx(qubitGetter(1), qubitGetter(0)); + circuit.rz(constant2, qubitGetter(0)); + circuit.rxx(constant0, qubitGetter(0), qubitGetter(1)); + circuit.ryy(constant2, qubitGetter(0), qubitGetter(1)); + // make series longer to enforce decomposition + circuit.rxx(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rzz(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rxx(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rzz(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rxx(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rzz(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rxx(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rzz(0.1, qubitGetter(1), qubitGetter(0)); + }; + + buildCircuit(comp, [](auto&& index) { return index; }); + + const auto module = importQuantumCircuit(comp); + ASSERT_TRUE(module); + ASSERT_TRUE(runPipeline(module.get()).succeeded()); + + const auto qco = buildQCOIR([&](qco::QCOProgramBuilder& b) { + auto reg = b.allocQubitRegister(2, "q"); + buildCircuit(b, [®](auto&& index) { return reg[index]; }); + }); + + const auto optimizedQco = buildQCOIR([&](qco::QCOProgramBuilder& b) { + auto reg = b.allocQubitRegister(2, "q"); + buildCircuit(b, [®](auto&& index) { return reg[index]; }); + }); + + verifyAllStages({ + .qcImport = nullptr, + .qcoConversion = qco.get(), + .optimization = optimizedQco.get(), + .qcConversion = nullptr, + .qirConversion = nullptr, + }); +} + } // namespace