diff --git a/CHANGELOG.md b/CHANGELOG.md index 24f3eb6aa8..04acee4f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,7 @@ releases may include breaking changes. #### Import and export - ✨ Add Qiskit circuit import and target-aware export to the compiler - collection ([#2031], [#2133], [#2140], [#2150], [#2175], [#2176]) + collection ([#2031], [#2133], [#2140], [#2150], [#2175], [#2176], [#2178]) ([**@burgholzer**], [**@simon1hofmann**]) - ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], [#1676], [#1706], [#1776], [#1836], [#1934], [#2000], [#2018], [#2105]) @@ -838,6 +838,7 @@ for previous changelogs._ [#2203]: https://github.com/munich-quantum-toolkit/core/pull/2203 [#2214]: https://github.com/munich-quantum-toolkit/core/pull/2214 [#2193]: https://github.com/munich-quantum-toolkit/core/pull/2193 +[#2178]: https://github.com/munich-quantum-toolkit/core/pull/2178 [#2176]: https://github.com/munich-quantum-toolkit/core/pull/2176 [#2175]: https://github.com/munich-quantum-toolkit/core/pull/2175 [#2169]: https://github.com/munich-quantum-toolkit/core/pull/2169 diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 21d47562b5..31ffcab7b7 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -11,6 +11,7 @@ #include "QiskitTranslation.h" #include "mlir/Dialect/QC/Translation/StandardGate.h" +#include #include // Qiskit requires its umbrella header before the extension function table. @@ -37,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -226,14 +226,40 @@ normalizePythonParameterLeaf(const nb::handle parameter) { throw std::runtime_error( "Qiskit parameter names cannot contain null characters"); } - auto result = Parameter::symbol(std::move(name)); const auto vectorElement = nb::module_::import_("qiskit.circuit").attr("ParameterVectorElement"); - if (nb::isinstance(parameter, vectorElement)) { + if (!nb::isinstance(parameter, vectorElement)) { + return Parameter::symbol(std::move(name)); + } + + const auto vector = pythonAttribute( + parameter, "vector", "Qiskit parameter-vector element has no vector"); + auto groupName = pythonStringAttribute( + vector, "name", "Qiskit parameter vector has an invalid name"); + auto groupIdentity = + pythonText(pythonAttribute(vector, "uuid", + "Qiskit parameter vector has no identity"), + "Qiskit parameter vector has an invalid identity"); + const auto groupIndex = pythonUnsignedAttribute( + parameter, "index", + "Qiskit parameter-vector element has an invalid index"); + size_t groupSize = 0U; + try { + groupSize = nb::len(vector); + } catch (const nb::python_error& error) { + throwPythonError("Qiskit parameter vector has an invalid size", error); + } + if (groupIdentity.empty() || groupIdentity.find('\0') != std::string::npos || + groupName.find('\0') != std::string::npos || + name != groupName + "[" + std::to_string(groupIndex) + "]") { throw std::runtime_error( - "Qiskit parameter-vector elements are not supported"); + "Qiskit parameter-vector element has invalid group metadata"); } - return result; + return Parameter::symbol(std::move(name), + ParameterGroup{.identity = std::move(groupIdentity), + .name = std::move(groupName), + .index = groupIndex, + .size = groupSize}); } struct ParsedParameter { @@ -1189,13 +1215,13 @@ class NativeControlFlowReader final : public ControlFlowReader { break; case QkLoopParamKind_Parameter: { auto symbol = qk_control_flow_loop_symbol_info(controlFlow_); - if (symbol.ty != QkSymbolType_Standalone) { + if (symbol.ty != QkSymbolType_Standalone && + symbol.ty != QkSymbolType_Element) { if (symbol.name != nullptr) { qk_str_free(symbol.name); } throw std::runtime_error( - "Qiskit indexed parameter-vector loop variables are not " - "supported"); + "Qiskit for-loop parameter has an unknown symbol type"); } if (symbol.name == nullptr) { throwPythonError("Qiskit failed to read a loop-parameter name"); @@ -1215,9 +1241,13 @@ class NativeControlFlowReader final : public ControlFlowReader { if (parameterSymbol == nullptr) { throw std::runtime_error("Qiskit for-loop parameter is not a symbol"); } - if (parameterSymbol->name != nativeName) { + const auto nativeIsElement = symbol.ty == QkSymbolType_Element; + const auto& group = parameterSymbol->group; + if (nativeIsElement != group.has_value() || + (group ? group->name : parameterSymbol->name) != nativeName || + (group && group->index != symbol.index)) { throw std::runtime_error( - "Qiskit Python and native loop-parameter names do not match"); + "Qiskit Python and native loop-parameter metadata do not match"); } result.parameter = std::move(parameter); } catch (const nb::python_error& error) { @@ -1897,7 +1927,17 @@ class PythonClassicalBuilder final { nb::object typesModule_; }; -using NativeSymbolTable = std::unordered_map; +struct NativeSymbol { + NativeSymbol(const std::string_view name, + std::optional sourceGroup) + : group(std::move(sourceGroup)), parameter(name) {} + + std::optional group; + OwnedParameter parameter; +}; + +using NativeSymbolTable = llvm::StringMap; +using PythonParameterGroups = llvm::StringMap; class NativeCircuitWriter final : public CircuitWriter { public: @@ -2073,13 +2113,15 @@ class NativeCircuitWriter final : public CircuitWriter { } [[nodiscard]] nb::object finish() override { - return finishImpl(false, nb::none(), nb::none()); + PythonParameterGroups groups; + return finishImpl(false, nb::none(), nb::none(), groups); } private: [[nodiscard]] nb::object finishImpl(const bool rebase, const nb::handle exactQubits, - const nb::handle exactClbits) { + const nb::handle exactClbits, + PythonParameterGroups& groups) { if (circuit_ == nullptr) { throw std::runtime_error( "Qiskit circuit writer has already been finalized"); @@ -2095,7 +2137,8 @@ class NativeCircuitWriter final : public CircuitWriter { pythonCircuit = rebaseCircuit(pythonCircuit, exactQubits, exactClbits); } replacePendingControlledUnitaries(pythonCircuit); - replacePendingControlFlow(pythonCircuit); + restoreParameterGroups(pythonCircuit, *symbols_, groups); + replacePendingControlFlow(pythonCircuit, groups); } catch (const nb::python_error& error) { throwPythonError("Qiskit failed to construct deferred instructions", error); @@ -2118,6 +2161,45 @@ class NativeCircuitWriter final : public CircuitWriter { std::vector> blockWriters; }; + static void restoreParameterGroups(const nb::handle circuit, + const NativeSymbolTable& symbols, + PythonParameterGroups& groups) { + if (!std::ranges::any_of(symbols, [](const auto& entry) { + return entry.second.group.has_value(); + })) { + return; + } + + const auto circuitModule = nb::module_::import_("qiskit.circuit"); + const auto parameterVector = circuitModule.attr("ParameterVector"); + const auto parameterVectorElement = + circuitModule.attr("ParameterVectorElement"); + nb::dict replacements; + const auto parameters = pythonAttribute( + circuit, "parameters", "Qiskit circuit has no parameter collection"); + for (const nb::handle parameter : nb::iter(parameters)) { + const auto name = pythonStringAttribute( + parameter, "name", "Qiskit circuit parameter has no name"); + const auto symbol = symbols.find(name); + if (symbol == symbols.end() || !symbol->second.group) { + continue; + } + const auto& metadata = *symbol->second.group; + const auto [group, inserted] = groups.try_emplace(metadata.identity); + if (inserted) { + group->second = parameterVector(metadata.name, metadata.size); + } + replacements[parameter] = + parameterVectorElement(group->second, metadata.index); + } + if (nb::len(replacements) == 0U) { + return; + } + pythonAttribute(circuit, "assign_parameters", + "Qiskit circuit cannot replace output parameters")( + replacements, nb::arg("inplace") = true, nb::arg("flat_input") = true); + } + void replacePendingControlledUnitaries(const nb::handle pythonCircuit) const { auto data = pythonAttribute(pythonCircuit, "data", "Qiskit circuit has no instruction data"); @@ -2183,17 +2265,13 @@ class NativeCircuitWriter final : public CircuitWriter { throw std::runtime_error( "Qiskit for-loop parameter has invalid symbol metadata"); } - const auto parameters = pythonAttribute( - body, "parameters", "Qiskit circuit has no parameter collection"); - for (const nb::handle parameter : nb::iter(parameters)) { - if (pythonStringAttribute(parameter, "name", - "Qiskit circuit parameter has no name") == - symbol->name) { - return nb::borrow(parameter); - } - } - throw std::runtime_error( - "Qiskit for-loop parameter is absent from its body"); + const auto parameterName = + symbol->group ? symbol->group->name + "[" + + std::to_string(symbol->group->index) + "]" + : symbol->name; + return pythonAttribute(body, "get_parameter", + "Qiskit circuit cannot find its loop parameter")( + parameterName); } [[nodiscard]] static nb::object constructControlFlowOperation( @@ -2239,7 +2317,8 @@ class NativeCircuitWriter final : public CircuitWriter { "Qiskit circuit export encountered an unsupported control-flow kind"); } - void replacePendingControlFlow(const nb::handle pythonCircuit) { + void replacePendingControlFlow(const nb::handle pythonCircuit, + PythonParameterGroups& groups) { auto data = pythonAttribute(pythonCircuit, "data", "Qiskit circuit has no instruction data"); const auto circuitQubits = pythonAttribute(pythonCircuit, "qubits", @@ -2263,7 +2342,7 @@ class NativeCircuitWriter final : public CircuitWriter { "Qiskit control-flow blocks use an incompatible writer"); } blocks.emplace_back( - writer->finishImpl(true, circuitQubits, circuitClbits)); + writer->finishImpl(true, circuitQubits, circuitClbits, groups)); } pending.blockWriters.clear(); auto operation = constructControlFlowOperation(pending, blocks, classical, @@ -2308,8 +2387,8 @@ class NativeCircuitWriter final : public CircuitWriter { throw std::runtime_error( "cannot export a symbolic parameter without a name"); } - return symbols_->try_emplace(symbol->name, symbol->name) - .first->second.get(); + return symbols_->try_emplace(symbol->name, symbol->name, symbol->group) + .first->second.parameter.get(); } auto output = std::make_unique(); diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index b90fea9f47..f75494f324 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -337,11 +338,39 @@ struct ExportState { ExportedParameters parameters; std::vector inputParameters; llvm::StringSet<> parameterNames; + ParameterGroupRegistry parameterGroups; size_t nextLoopParameter = 0U; uint32_t numQubits = 0; uint32_t numClbits = 0; }; +[[nodiscard]] ParameterGroup parameterGroup(const mlir::Attribute attribute) { + const auto metadata = llvm::dyn_cast(attribute); + if (!metadata || metadata.size() != 4U) { + throw std::runtime_error( + "Qiskit circuit export requires complete and valid parameter-group " + "metadata"); + } + const auto identity = metadata.getAs("identity"); + const auto name = metadata.getAs("name"); + const auto index = metadata.getAs("index"); + const auto size = metadata.getAs("size"); + if (!identity || !name || !index || !size || identity.getValue().empty() || + identity.getValue().contains('\0') || name.getValue().contains('\0') || + !index.getType().isInteger(64) || index.getInt() < 0 || + !size.getType().isInteger(64) || size.getInt() < 0) { + throw std::runtime_error( + "Qiskit circuit export requires complete and valid parameter-group " + "metadata"); + } + return { + .identity = identity.str(), + .name = name.str(), + .index = static_cast(index.getInt()), + .size = static_cast(size.getInt()), + }; +} + [[nodiscard]] bool parameterUsesName(const Parameter& parameter, const std::string_view name) { if (const auto* symbol = parameter.getSymbol()) { @@ -438,7 +467,20 @@ void collectParameters(mlir::func::FuncOp function, ExportState& state) { throw std::runtime_error( "Qiskit circuit export requires unique parameter names"); } - auto parameter = Parameter::symbol(name.str()); + + const auto groupAttribute = function.getArgAttr( + index, mlir::mqt::MQTDialect::ParameterGroupAttrHelper::getNameStr()); + std::optional group; + if (groupAttribute) { + group = parameterGroup(groupAttribute); + if (name.getValue() != + group->name + "[" + std::to_string(group->index) + "]") { + throw std::runtime_error( + "Qiskit parameter input name does not match its group and index"); + } + state.parameterGroups.add(*group); + } + auto parameter = Parameter::symbol(name.str(), std::move(group)); state.parameters[argument] = parameter; state.inputParameters.push_back(std::move(parameter)); } @@ -1659,6 +1701,12 @@ collectFor(mlir::scf::ForOp loop, ExportState& state, result->kind = ControlFlowKind::For; result->loop = { .isRange = true, .start = *lower, .stop = *upper, .step = *step}; + std::optional sourceGroup; + if (const auto attribute = loop->getAttr( + mlir::mqt::MQTDialect::ParameterGroupAttrHelper::getNameStr())) { + sourceGroup = parameterGroup(attribute); + state.parameterGroups.add(*sourceGroup); + } std::optional projection; std::optional loopParameter; std::string loopParameterName; @@ -1678,7 +1726,7 @@ collectFor(mlir::scf::ForOp loop, ExportState& state, loopParameterName = "_mqt_loop_" + std::to_string(identity); } while (state.parameterNames.contains(loopParameterName)); state.parameterNames.insert(loopParameterName); - loopParameter = Parameter::symbol(loopParameterName); + loopParameter = Parameter::symbol(loopParameterName, sourceGroup); state.parameters[projection->value] = *loopParameter; } } diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 24dcd675a0..cfb5bcd61d 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -88,6 +89,24 @@ constexpr size_t MAX_EXPANDED_OPERATIONS = 10'000'000U; [[nodiscard]] mlir::Value floatConstant(mlir::ImplicitLocOpBuilder& builder, double value); +[[nodiscard]] mlir::DictionaryAttr +parameterGroupAttribute(mlir::Builder& builder, const ParameterGroup& group) { + if (group.index > + static_cast(std::numeric_limits::max()) || + group.size > static_cast(std::numeric_limits::max())) { + throw std::runtime_error( + "Qiskit parameter-vector metadata cannot be represented by MLIR"); + } + return builder.getDictionaryAttr({ + builder.getNamedAttr("identity", builder.getStringAttr(group.identity)), + builder.getNamedAttr("name", builder.getStringAttr(group.name)), + builder.getNamedAttr("index", builder.getI64IntegerAttr( + static_cast(group.index))), + builder.getNamedAttr( + "size", builder.getI64IntegerAttr(static_cast(group.size))), + }); +} + [[noreturn]] void throwImportedParameterExpressionSizeError() { throw std::runtime_error( "Qiskit parameter expression exceeds the supported " + @@ -137,10 +156,20 @@ void validateParameterImpl(const Parameter& parameter, throw std::runtime_error( "Qiskit returned a parameter with invalid symbol metadata"); } - if (localParameters.contains(symbol->name)) { - return; - } - if (freeParameters.contains(symbol->name)) { + const auto validateKnownSymbol = [&](const ValidationParameters& known) { + const auto found = known.find(symbol->name); + if (found == known.end()) { + return false; + } + const auto* expected = found->second.getSymbol(); + if (expected == nullptr || expected->group != symbol->group) { + throw std::runtime_error("Qiskit parameter symbol '" + symbol->name + + "' has conflicting group metadata"); + } + return true; + }; + if (validateKnownSymbol(localParameters) || + validateKnownSymbol(freeParameters)) { return; } throw std::runtime_error("Qiskit parameter symbol '" + symbol->name + @@ -1148,6 +1177,7 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, requireExactLoopParameter(loop.start); requireExactLoopParameter(loop.step > 0 ? loop.stop - 1 : loop.stop + 1); } + auto* const containingBlock = builder.getInsertionBlock(); builder.scfFor(0, count, 1, [&](const mlir::Value iteration) { auto parameters = localParameters; if (loop.parameter) { @@ -1159,6 +1189,15 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, } translateBlock(*body, parameters); }); + if (loop.parameter) { + const auto* symbol = loop.parameter->getSymbol(); + if (symbol != nullptr && symbol->group) { + mlir::cast(&containingBlock->back()) + ->setAttr( + mlir::mqt::MQTDialect::ParameterGroupAttrHelper::getNameStr(), + parameterGroupAttribute(builder, *symbol->group)); + } + } return; } case ControlFlowKind::Switch: { @@ -1911,6 +1950,7 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { const auto freeParameters = view->parameters(); ValidationParameters freeParameterSymbols; llvm::StringSet<> parameterNames; + ParameterGroupRegistry parameterGroups; for (const auto& parameter : freeParameters) { const auto* symbol = parameter.getSymbol(); if (symbol == nullptr || symbol->name.empty()) { @@ -1921,6 +1961,15 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { throw std::runtime_error( "Qiskit circuit contains distinct parameters with the same name"); } + if (symbol->group) { + const auto& group = *symbol->group; + if (group.index > + static_cast(std::numeric_limits::max())) { + throw std::runtime_error( + "Qiskit parameter-vector index cannot be represented by MLIR"); + } + parameterGroups.add(group); + } freeParameterSymbols.try_emplace(symbol->name, parameter); } @@ -1963,14 +2012,15 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { GlobalParameters globalParameters; for (const auto& parameter : freeParameters) { const auto* symbol = parameter.getSymbol(); - if (symbol == nullptr) { - throw std::runtime_error( - "Qiskit circuit returned an invalid free parameter"); - } - const llvm::SmallVector argumentAttributes{ + llvm::SmallVector argumentAttributes{ builder.getNamedAttr( mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr(), builder.getStringAttr(symbol->name))}; + if (symbol->group) { + argumentAttributes.push_back(builder.getNamedAttr( + mlir::mqt::MQTDialect::ParameterGroupAttrHelper::getNameStr(), + parameterGroupAttribute(builder, *symbol->group))); + } const auto index = function.getNumArguments(); // MLIR types are handles. Converting FloatType to Type keeps the same // storage and does not slice object state. diff --git a/bindings/mlir/qiskit/QiskitTranslation.cpp b/bindings/mlir/qiskit/QiskitTranslation.cpp index aca897043a..e166301715 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.cpp +++ b/bindings/mlir/qiskit/QiskitTranslation.cpp @@ -21,6 +21,24 @@ namespace mqt::bindings::qiskit { +void ParameterGroupRegistry::add(const ParameterGroup& group) { + const auto [known, inserted] = groups.try_emplace(group.identity, group); + if (inserted) { + if (group.size > MAX_PARAMETER_GROUP_SIZE - totalSize) { + throw std::runtime_error( + "Qiskit circuit translation supports at most " + + std::to_string(MAX_PARAMETER_GROUP_SIZE) + + " elements across all distinct parameter vectors"); + } + totalSize += group.size; + return; + } + if (known->second.name != group.name || known->second.size != group.size) { + throw std::runtime_error( + "one Qiskit parameter group has conflicting metadata"); + } +} + uint32_t validateRegisterLayout(const std::vector& registers, const uint32_t total, const std::string_view kind) { diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index c7bdc5490c..a66a20e5d3 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -12,6 +12,7 @@ #include "mlir/Dialect/QC/Translation/StandardGate.h" +#include #include #include @@ -52,6 +53,26 @@ validateRegisterLayout(const std::vector& registers, uint32_t total, inline constexpr size_t MAX_PARAMETER_EXPRESSION_DEPTH = 64U; inline constexpr size_t MAX_PARAMETER_EXPRESSION_NODES = 4096U; +inline constexpr uint64_t MAX_PARAMETER_GROUP_SIZE = 65'536U; + +/** Source-level vector metadata for one scalar parameter. */ +struct ParameterGroup { + std::string identity; + std::string name; + uint64_t index = 0U; + uint64_t size = 0U; + + [[nodiscard]] bool operator==(const ParameterGroup&) const = default; +}; + +class ParameterGroupRegistry { +public: + void add(const ParameterGroup& group); + +private: + llvm::StringMap groups; + uint64_t totalSize = 0U; +}; enum class UnaryParameterKind : uint8_t { Negate, @@ -84,6 +105,7 @@ class Parameter { struct Symbol { std::string name; + std::optional group; }; struct Unary { @@ -103,8 +125,10 @@ class Parameter { return Parameter(Number{value}); } - [[nodiscard]] static Parameter symbol(std::string name) { - return Parameter(Symbol{std::move(name)}); + [[nodiscard]] static Parameter + symbol(std::string name, std::optional group = std::nullopt) { + return Parameter( + Symbol{.name = std::move(name), .group = std::move(group)}); } [[nodiscard]] static Parameter unary(const UnaryParameterKind operation, diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md index 49bc54678f..94d39ca625 100644 --- a/docs/mlir/python_compiler_collection.md +++ b/docs/mlir/python_compiler_collection.md @@ -175,7 +175,7 @@ flow, so export uses Qiskit's public Python classes for these operations. | Clbit and ClassicalRegister expression variables | Supported | Supported | | Standalone classical runtime variables | Rejected | Rejected | | Free symbols and supported real parameter expressions | Supported | Supported | -| Parameter-vector elements | Rejected | Not emitted | +| Parameter-vector elements | Supported | Supported | | Dense numeric unitaries up to eight qubits | Supported | Supported | | Register aliases or interleaved membership | Rejected | Rejected | | Transpiler layout metadata | Accepted and ignored | Not emitted | @@ -185,19 +185,22 @@ containing circuit. This includes values used only by the condition or switch target and not by a control-flow block. Standalone runtime variables remain unsupported. -Free standalone symbols become named {code}`f64` program inputs. -Parameter-vector elements are rejected because converting them to standalone -parameters would change positional binding order. Standalone parameter names -that contain brackets remain ordinary scalar names. Parameter-expression trees -support at most 64 levels and 4,096 nodes. Import and export support real -addition, subtraction, multiplication, division, power, negation, trigonometric -and inverse trigonometric functions, exponential, logarithm, absolute value, and -real conjugation. Other parameter-expression functions are rejected. Lexically -bound {code}`for`-loop induction parameters are supported and remain distinct -from free symbols. Parameterized custom-instruction definitions are expanded -after their symbols and expressions are resolved. Definition expansion rejects -missing definitions, cycles, operand arity mismatches, nesting beyond 64 levels, -and more than 10 million expanded operations. +Free symbols become named {code}`f64` program inputs. Parameter-vector elements +retain their grouping and index, preserving vector order and positional binding +across a round trip; similarly named standalone parameters remain standalone. +Elements used in different structured-control blocks are restored into one +shared vector for the complete circuit tree. Free parameter vectors and their +combined declared size in one translated circuit are each limited to 65,536 +elements. Parameter-expression trees support at most 64 levels and 4,096 nodes. +Import and export support real addition, subtraction, multiplication, division, +power, negation, trigonometric and inverse trigonometric functions, exponential, +logarithm, absolute value, and real conjugation. Other parameter-expression +functions are rejected. Lexically bound {code}`for`-loop induction parameters +are supported and remain distinct from free symbols. Parameterized +custom-instruction definitions are expanded after their symbols and expressions +are resolved. Definition expansion rejects missing definitions, cycles, operand +arity mismatches, nesting beyond 64 levels, and more than 10 million expanded +operations. Structured-control export accepts result-free {code}`scf.if`, constant-range {code}`scf.for` without loop-carried values, expression-based {code}`scf.while` diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 5471ec3976..2d34f55a38 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -21,6 +21,9 @@ def MQTDialect : Dialect { across quantum dialect conversions. It defines no operations or types. `mqt.input_name` records the source-level name of a function input. + `mqt.parameter_group` optionally preserves the source-level vector + identity, name, element index, and size of a function input or a lexically + bound `scf.for` parameter. `mqt.register_name` records the source-level name of a quantum or classical register allocation. Input and register names share one function-wide namespace. @@ -29,6 +32,7 @@ def MQTDialect : Dialect { }]; let discardableAttrs = (ins "::mlir::StringAttr":$input_name, + "::mlir::DictionaryAttr":$parameter_group, "::mlir::StringAttr":$register_name, "::mlir::UnitAttr":$entry_point); let hasOperationAttrVerify = 1; diff --git a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp index bbc6ce542b..4b2b6107d9 100644 --- a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp +++ b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp @@ -890,6 +890,7 @@ struct ConvertQCOSCFForOp final : OpConversionPattern { auto newFor = scf::ForOp::create( rewriter, op.getLoc(), adaptor.getLowerBound(), adaptor.getUpperBound(), adaptor.getStep(), classicalInits); + newFor->setDiscardableAttrs(op->getDiscardableAttrDictionary()); // Erase default block rewriter.eraseBlock(&newFor.getRegion().front()); diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 14a384c24f..a737ae1387 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -1459,6 +1459,7 @@ struct ConvertSCFForOp final : StatefulOpConversionPattern { auto newForOp = scf::ForOp::create(rewriter, op.getLoc(), op.getLowerBound(), op.getUpperBound(), op.getStep(), initArgs); + newForOp->setDiscardableAttrs(op->getDiscardableAttrDictionary()); assignMappedTensors(state, op.getOperation(), registerMap, newForOp.getResults() diff --git a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt index 871f7e2283..aafd4f293b 100644 --- a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt @@ -21,6 +21,7 @@ add_mlir_dialect_library( MLIRMemRefDialect MLIRQCDialect MLIRQCODialect + MLIRSCFDialect MLIRQTensorDialect) mqt_mlir_target_use_project_options(MLIRMQTDialect) diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index f1da715d93..1cff286d04 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,8 @@ #include #include +#include + using namespace mlir; using namespace mlir::mqt; @@ -82,6 +85,59 @@ verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { return success(); } +[[nodiscard]] static LogicalResult +verifyParameterGroup(Operation* operation, const Attribute attribute) { + const auto group = dyn_cast(attribute); + const auto identity = group ? group.getAs("identity") : nullptr; + const auto groupName = group ? group.getAs("name") : nullptr; + const auto groupIndex = group ? group.getAs("index") : nullptr; + const auto groupSize = group ? group.getAs("size") : nullptr; + if (!group || group.size() != 4U || !identity || !groupName || !groupIndex || + !groupSize) { + return operation->emitError() + << "parameter-group metadata must contain exactly identity, " + "name, index, and size"; + } + if (identity.getValue().empty() || identity.getValue().contains('\0') || + groupName.getValue().contains('\0')) { + return operation->emitError() + << "parameter-group string metadata is invalid"; + } + if (!groupIndex.getType().isInteger(64) || + groupIndex.getValue().isNegative() || + !groupSize.getType().isInteger(64) || groupSize.getValue().isNegative()) { + return operation->emitError() + << "parameter-group index and size must be nonnegative i64 " + "integers"; + } + return success(); +} + +[[nodiscard]] static LogicalResult +verifyInputGroup(FunctionOpInterface function, Operation* operation, + const unsigned argIndex, const Attribute attribute) { + const auto inputName = function.getArgAttrOfType( + argIndex, MQTDialect::InputNameAttrHelper::getNameStr()); + if (!inputName) { + return operation->emitError() + << "parameter-group metadata on a function argument requires an " + "input name"; + } + if (failed(verifyParameterGroup(operation, attribute))) { + return failure(); + } + const auto group = cast(attribute); + const auto groupName = group.getAs("name"); + const auto groupIndex = group.getAs("index"); + const auto expectedName = + groupName.str() + "[" + std::to_string(groupIndex.getInt()) + "]"; + if (inputName.getValue() != expectedName) { + return operation->emitError() + << "parameter input name must match its group name and index"; + } + return success(); +} + [[nodiscard]] static bool isRegisterAllocation(Operation* operation) { if (isa(operation)) { return true; @@ -147,6 +203,14 @@ MQTDialect::verifyOperationAttribute(Operation* operation, if (attribute.getName() == RegisterNameAttrHelper::getNameStr()) { return verifyRegisterName(operation, attribute); } + if (attribute.getName() == ParameterGroupAttrHelper::getNameStr()) { + if (!isa(operation)) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' is only valid on scf.for"; + } + return verifyParameterGroup(operation, attribute.getValue()); + } if (attribute.getName() == InputNameAttrHelper::getNameStr()) { return operation->emitError() << "attribute '" << attribute.getName().getValue() @@ -159,14 +223,13 @@ MQTDialect::verifyOperationAttribute(Operation* operation, LogicalResult MQTDialect::verifyRegionArgAttribute( Operation* operation, const unsigned regionIndex, const unsigned argIndex, const NamedAttribute attribute) { - if (attribute.getName() != InputNameAttrHelper::getNameStr()) { + const auto attributeName = attribute.getName(); + if (attributeName != InputNameAttrHelper::getNameStr() && + attributeName != ParameterGroupAttrHelper::getNameStr()) { return operation->emitError() << "attribute '" << attribute.getName().getValue() << "' is not valid on a region argument"; } - if (failed(verifyName(operation, attribute))) { - return failure(); - } auto function = dyn_cast(operation); if (!function || regionIndex != 0) { @@ -175,6 +238,14 @@ LogicalResult MQTDialect::verifyRegionArgAttribute( << "' requires a function entry-block argument"; } + if (attributeName == ParameterGroupAttrHelper::getNameStr()) { + return verifyInputGroup(function, operation, argIndex, + attribute.getValue()); + } + if (failed(verifyName(operation, attribute))) { + return failure(); + } + const auto name = cast(attribute.getValue()); for (unsigned index = 0; index < function.getNumArguments(); ++index) { if (index == argIndex) { diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 6099633f98..363efcbd48 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -56,7 +56,12 @@ class MQTIRTest : public ::testing::Test { TEST_F(MQTIRTest, AcceptsProgramInputAndRegisterNames) { EXPECT_TRUE(parse(R"mlir( module { - func.func @qc(%theta: f64 {mqt.input_name = "theta"}) { + func.func @qc(%theta: f64 {mqt.input_name = "theta[2]", + mqt.parameter_group = {identity = "group-id", name = "theta", + index = 2 : i64, size = 4 : i64}}, + %element: f64 {mqt.input_name = "[0]", + mqt.parameter_group = {identity = "empty-name", name = "", + index = 0 : i64, size = 1 : i64}}) { %reg = memref.alloc() {mqt.register_name = "q"} : memref<2x!qc.qubit> return @@ -161,7 +166,44 @@ TEST_F(MQTIRTest, RejectsDuplicateInputNames) { )mlir")); } -TEST_F(MQTIRTest, RejectsInputNameOnOperation) { +TEST_F(MQTIRTest, RejectsInvalidInputGroups) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @incomplete(%arg: f64 {mqt.input_name = "theta[0]", + mqt.parameter_group = {identity = "group"}}) { return } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @empty_identity(%arg: f64 {mqt.input_name = "theta[0]", + mqt.parameter_group = {identity = "", name = "theta", + index = 0 : i64, size = 1 : i64}}) { return } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @null_name(%arg: f64 {mqt.input_name = "theta[0]", + mqt.parameter_group = {identity = "group", name = "theta\00", + index = 0 : i64, size = 1 : i64}}) { return } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @wrong_integer_type(%arg: f64 {mqt.input_name = "theta[0]", + mqt.parameter_group = {identity = "group", name = "theta", + index = 0 : i32, size = 1 : i64}}) { return } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @wrong_name(%arg: f64 {mqt.input_name = "phi[0]", + mqt.parameter_group = {identity = "group", name = "theta", + index = 0 : i64, size = 1 : i64}}) { return } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsInputMetadataOnOperations) { EXPECT_FALSE(parse(R"mlir( module { func.func @main() { @@ -171,6 +213,17 @@ TEST_F(MQTIRTest, RejectsInputNameOnOperation) { } } )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %c0 = "arith.constant"() { + mqt.parameter_group = {identity = "group", name = "theta", + index = 0 : i64, size = 1 : i64}, + value = 0.0 : f64} : () -> f64 + return + } + } + )mlir")); } TEST_F(MQTIRTest, RejectsInvalidRegisterNamesAndOwners) { diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 7d50f83e40..7bdee690b1 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -37,6 +37,7 @@ ) from qiskit.circuit.classical import expr, types from qiskit.circuit.controlflow import CASE_DEFAULT, IfElseOp +from qiskit.circuit.parametervector import ParameterVectorElement from qiskit.quantum_info import Operator, random_unitary from mqt.core.mlir import CompilerTarget, QCProgram, compile_program @@ -2193,18 +2194,58 @@ def test_direct_symbolic_parameters_round_trip_with_shared_identity() -> None: ) -def test_parameter_vector_elements_fail_import_without_mutation() -> None: - """Reject vector elements until the provenance follow-up is applied.""" +def test_sparse_parameter_vector_round_trip_preserves_order_and_binding() -> None: + """Preserve a sparse vector's grouping, size, and numeric element order.""" + vector = ParameterVector("theta", 12) + circuit = QuantumCircuit(1, global_phase=vector[0]) + circuit.rx(vector[10] + vector[2], 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + parameters = list(restored.parameters) + assert [parameter.index for parameter in parameters] == [0, 2, 10] + restored_vector = parameters[0].vector + assert len(restored_vector) == len(vector) + values = [0.01 * index for index in range(12)] + assert Operator(restored.assign_parameters({restored_vector: values}, strict=False)).equiv( + Operator(circuit.assign_parameters({vector: values}, strict=False)) + ) + + +def test_parameter_vector_is_shared_across_sibling_blocks() -> None: + """Restore one vector across parent and sibling control-flow blocks.""" vector = ParameterVector("theta", 2) + circuit = QuantumCircuit(1, 1) + circuit.rz(vector[0], 0) + with circuit.if_test((circuit.clbits[0], True)) as else_: + circuit.rx(vector[0], 0) + with else_: + circuit.ry(vector[1], 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + root_parameter = restored.data[0].operation.params[0] + blocks = restored.data[1].operation.blocks + parameters = [root_parameter, *(block.data[0].operation.params[0] for block in blocks)] + assert [parameter.index for parameter in parameters] == [0, 0, 1] + restored.assign_parameters({parameters[0].vector: [0.25, 0.5]}, inplace=True) + assert not restored.parameters + + +def test_distinct_parameter_vectors_with_the_same_name_remain_distinct() -> None: + """Keep opaque group identity when vector display names coincide.""" + first = ParameterVector("theta", 3) + second = ParameterVector("theta", 3) circuit = QuantumCircuit(1) - circuit.rx(vector[0], 0) - source_data = list(circuit.data) + circuit.rx(first[0], 0) + circuit.ry(second[2], 0) - with pytest.raises(RuntimeError, match="parameter-vector elements are not supported"): - QCProgram.from_qiskit(circuit) + restored = QCProgram.from_qiskit(circuit).to_qiskit() - assert list(circuit.data) == source_data - assert circuit.parameters == {vector[0]} + parameters = list(restored.parameters) + assert [parameter.index for parameter in parameters] == [0, 2] + assert parameters[0].vector.uuid != parameters[1].vector.uuid + assert Operator(restored.assign_parameters([0.2, 0.7])).equiv(Operator(circuit.assign_parameters([0.2, 0.7]))) def test_standalone_bracket_parameter_names_remain_standalone() -> None: @@ -2215,15 +2256,95 @@ def test_standalone_bracket_parameter_names_remain_standalone() -> None: circuit.rx(theta_ten, 0) circuit.ry(theta_two, 0) - program = QCProgram.from_qiskit(circuit) - restored = program.to_qiskit() + restored = QCProgram.from_qiskit(circuit).to_qiskit() - assert "mqt.input_group" not in program.ir + assert all(not isinstance(parameter, ParameterVectorElement) for parameter in restored.parameters) assert {parameter.name for parameter in restored.parameters} == {"theta[2]", "theta[10]"} values = [0.1, 0.2] assert Operator(restored.assign_parameters(values)).equiv(Operator(circuit.assign_parameters(values))) +def test_parameter_vector_element_is_valid_loop_parameter() -> None: + """Preserve a vector-element loop symbol as a lexical parameter.""" + iteration = ParameterVector("iteration", 4)[2] + body = QuantumCircuit(1) + body.rx(iteration, 0) + circuit = QuantumCircuit(1) + circuit.for_loop(range(3), iteration, body, [0], [], label=None) + + program = QCProgram.from_qiskit(circuit) + assert program.ir.count("mqt.parameter_group") == 1 + restored_circuits = ( + program.to_qiskit(), + program.to_qco(copy=True).to_qc().to_qiskit(), + ) + for restored in restored_circuits: + restored_loop = restored.data[0].operation + restored_parameter = restored_loop.params[1] + assert restored_parameter.vector.name == "iteration" + assert restored_parameter.index == 2 + assert len(restored_parameter.vector) == 4 + assert restored_loop.blocks[0].data[0].operation.params[0] == restored_parameter + assert not restored.parameters + + +@pytest.mark.parametrize(("size", "index"), [(0, 0), (1, 1)]) +def test_parameter_vector_element_outside_current_size_round_trips(size: int, index: int) -> None: + """Preserve a vector element outside its vector's current size.""" + vector = ParameterVector("theta", size) + circuit = QuantumCircuit(1) + circuit.rx(ParameterVectorElement(vector, index), 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + restored_element = next(iter(restored.parameters)) + assert restored_element.index == index + assert len(restored_element.vector) == size + + +@pytest.mark.parametrize("sizes", [[65_537], [32_769, 32_769]]) +def test_parameter_vector_size_limits_on_import(sizes: list[int]) -> None: + """Bound individual and aggregate vector metadata before MLIR creation.""" + circuit = QuantumCircuit(1) + for index, size in enumerate(sizes): + circuit.rx(ParameterVector(f"theta{index}", size)[0], 0) + + with pytest.raises(RuntimeError, match="across all distinct"): + QCProgram.from_qiskit(circuit) + + +@pytest.mark.parametrize( + ("sizes", "shared_group_id", "message"), + [ + ([65_537], None, "across all distinct"), + ([32_769, 32_769], None, "across all distinct"), + ([1, 2], 0, "conflicting metadata"), + ], +) +def test_parameter_vector_metadata_is_preflighted(sizes: list[int], shared_group_id: int | None, message: str) -> None: + """Validate vector consistency and resource bounds before allocation.""" + arguments = [] + gates = [] + for index, size in enumerate(sizes): + arguments.append( + f'%theta{index}: f64 {{mqt.input_name = "theta{index}[0]", ' + f'mqt.parameter_group = {{identity = "group{index if shared_group_id is None else shared_group_id}", ' + f'name = "theta{index}", index = 0 : i64, size = {size} : i64}}}}' + ) + gates.append(f" qc.rx(%theta{index}) %q : !qc.qubit") + program = QCProgram.from_mlir_str( + "module {\n" + f" func.func @main({', '.join(arguments)}) attributes {{mqt.entry_point}} {{\n" + " %q = qc.alloc : !qc.qubit\n" + "\n".join(gates) + "\n qc.dealloc %q : !qc.qubit\n" + " return\n" + " }\n" + "}\n" + ) + + with pytest.raises(RuntimeError, match=message): + program.to_qiskit() + + def _assign_parameter_values(circuit: QuantumCircuit, values: dict[str, float]) -> QuantumCircuit: """Bind a circuit using parameter names after an import/export round trip.