From ce8ab1dbfaba0502593048e134fff7b0af8af4ff Mon Sep 17 00:00:00 2001 From: River McCubbin Date: Mon, 20 Jul 2026 16:38:50 -0400 Subject: [PATCH 1/8] add pass id support --- .../lit/GraphDecomposition/TestGraphOpId.mlir | 31 +++++++++++++++++++ .../graph_decomposition.cpp | 8 +++++ 2 files changed, 39 insertions(+) create mode 100644 frontend/test/lit/GraphDecomposition/TestGraphOpId.mlir diff --git a/frontend/test/lit/GraphDecomposition/TestGraphOpId.mlir b/frontend/test/lit/GraphDecomposition/TestGraphOpId.mlir new file mode 100644 index 0000000000..8536a5fa85 --- /dev/null +++ b/frontend/test/lit/GraphDecomposition/TestGraphOpId.mlir @@ -0,0 +1,31 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test that graph-decomposition succeeds when using graphOpIds + +// RUN: catalyst --tool=opt --split-input-file --pass-pipeline='builtin.module(graph-decomposition{gate-set=PauliX=1.0 alt-decomps=Hadamard=my_decomp})' %s | FileCheck %s + +func.func @circuit(%q: !quantum.bit) -> !quantum.bit { + // CHECK-NOT: Hadamard + // CHECK: PauliX + // CHECK: PauliX + %out = quantum.custom "Hadamard"() %q: !quantum.bit + return %out: !quantum.bit +} + +func.func private @my_decomp(%q: !quantum.bit) -> !quantum.bit attributes {target_gate="Hadamard[][1]{}"} { + %q0 = quantum.custom "PauliX"() %q : !quantum.bit + %q1 = quantum.custom "PauliX"() %q0 : !quantum.bit + return %q1 : !quantum.bit +} diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp index 39929153a4..027b54d8fe 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp @@ -453,6 +453,10 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase(op.getOperation())) { + node.id = decompGate.getGraphOpId(); + } + operators.push_back(node); }); } @@ -476,6 +480,10 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase Date: Wed, 22 Jul 2026 12:34:37 -0400 Subject: [PATCH 2/8] migrate pass & solver to ID --- doc/releases/changelog-dev.md | 1 + .../DecompGraphSolver/DGBuilder.cpp | 14 +- .../DecompGraphSolver/DGBuilder.hpp | 1 + .../DecompGraphSolver/DGSolver.cpp | 4 +- .../DecompGraphSolver/DGSolver.hpp | 2 - .../DecompGraphSolver/DGTypes.hpp | 66 ++----- .../DecompGraphSolver/DGUtils.hpp | 1 + .../graph_decomposition.cpp | 22 +-- .../Test_DecompGraphCore.cpp | 139 +++++--------- .../Test_DecompGraphSolver.cpp | 173 +++++++----------- 10 files changed, 143 insertions(+), 280 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index f6ac0cf325..589bba71f6 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -313,6 +313,7 @@ [(#2983)](https://github.com/PennyLaneAI/catalyst/pull/2983) [(#3022)](https://github.com/PennyLaneAI/catalyst/pull/3022) [(#3039)](https://github.com/PennyLaneAI/catalyst/pull/3039) + [(#3046)](https://github.com/PennyLaneAI/catalyst/pull/3046) * The `graph-decomposition` pass eliminates three redundant IR manipulations: the cloning, removal, and re-insertion of user rules. This optimization is particularly diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp index 7d2724941c..97187aa0a3 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp @@ -18,13 +18,23 @@ #include "DGBuilder.hpp" +#include #include #include +#include +#include +#include +#include #include +#include -#include "DGUtils.hpp" +#include "boost/graph/adjacency_list.hpp" +#include "boost/graph/detail/adjacency_list.hpp" +#include "boost/graph/graph_selectors.hpp" +#include "boost/graph/graph_traits.hpp" -#include +#include "DGTypes.hpp" +#include "DGUtils.hpp" using namespace DecompGraph::Core; diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.hpp index 984dd6a970..56fae1952b 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.hpp @@ -24,6 +24,7 @@ #pragma once +#include #include #include diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.cpp index c83ccddce5..35b7d760ae 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.cpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.cpp @@ -18,12 +18,14 @@ #include "DGSolver.hpp" -#include #include +#include #include +#include #include #include "DGTypes.hpp" +#include "DGUtils.hpp" using namespace DecompGraph::Core; diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.hpp index 9e92a32ebd..030481b0b7 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.hpp @@ -25,14 +25,12 @@ #pragma once -#include #include #include #include #include "DGBuilder.hpp" #include "DGTypes.hpp" -#include "DGUtils.hpp" namespace DecompGraph::Solver { diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp index db092e74c8..cd4aaeb844 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp @@ -53,39 +53,16 @@ namespace DecompGraph::Core { * when adding support for operators with dynamic numbers of wires/params. */ struct OperatorNode { - std::string name; - int numWires{-1}; - int numParams{-1}; + std::string id; bool adjoint{false}; - // Optional static arguments for operators that require additional data. + // optional params, primarily for debug use + std::string name{""}; + int numWires{-1}; + int numParams{-1}; std::unordered_map staticNamedArgs{}; - std::string id{""}; - bool operator==(const OperatorNode &other) const - { - // id match - if (!id.empty() && !other.id.empty() && id == other.id) { - return true; - } - // legacy fallback if either op is missing ID - - // For equality, we consider numWires and numParams conditionally equal - // if they are not set to -1 (which indicates a wildcard that can match any value). - const bool default_wires = - (numWires == -1 || other.numWires == -1 || numWires == other.numWires); - const bool default_params = - (numParams == -1 || other.numParams == -1 || numParams == other.numParams); - - // Static arguments are optional: if either side has no static args, they - // are treated as matching (wildcard). When both sides provide entries, the maps must - // be equal element-wise for the operators to be considered equivalent. - const bool static_args_match = staticNamedArgs.empty() || other.staticNamedArgs.empty() || - staticNamedArgs == other.staticNamedArgs; - - return name == other.name && default_wires && default_params && adjoint == other.adjoint && - static_args_match; - } + bool operator==(const OperatorNode &other) const { return id == other.id; } bool operator!=(const OperatorNode &other) const { return !(*this == other); } }; @@ -107,11 +84,7 @@ struct OperatorNode { struct OperatorNodeHash { std::size_t operator()(const OperatorNode &node) const { - // prefer id if available - if (!node.id.empty()) { - return std::hash{}(node.id); - } - return std::hash{}(node.name); + return std::hash{}(node.id); } }; @@ -119,33 +92,16 @@ struct OperatorNodeHash { * @brief This represents the weighted target gateset for the graph decomposition problem. */ struct WeightedGateset { + // TODO: using ID here mandates that gatesets specify all legal IDs, rather than generic class + // like "PauliRot". This should be updated to work on generic names std::unordered_map ops; - [[nodiscard]] bool contains(const OperatorNode &op) const - { - // hash match - if (ops.find(op) != ops.end()) { - return true; - } - - // use op-matching if hashes failed (could be id vs name) - for (auto [gatesetOp, cost] : ops) { - if (gatesetOp == op) { - return true; - } - } - - return false; - } + [[nodiscard]] bool contains(const OperatorNode &op) const { return ops.find(op) != ops.end(); } [[nodiscard]] double getCost(const OperatorNode &op) const { auto it = ops.find(op); - if (it != ops.end()) { - return it->second; - } - - return std::numeric_limits::infinity(); + return it != ops.end() ? it->second : std::numeric_limits::infinity(); } }; diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp index e962638a33..27c66217ef 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGUtils.hpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include "DGTypes.hpp" diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp index 027b54d8fe..c43ea88555 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/graph_decomposition.cpp @@ -422,7 +422,7 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase(op.getOperation())) { - node.name = customOp.getGateName().str(); - } - // Name handling for non-custom ops - else { - std::string name = op->getName().stripDialect().str(); - if (name == "gphase") { - name = "GlobalPhase"; - } - else if (name == "paulirot") { - name = cast(op.getOperation()).getGraphOpId(); - } - node.name = name; - } + node.name = op.getOperatorName(); + node.id = op.getGraphOpId(); if (auto paramOp = llvm::dyn_cast(op.getOperation())) { @@ -453,10 +441,6 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase(op.getOperation())) { - node.id = decompGate.getGraphOpId(); - } - operators.push_back(node); }); } diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp index 5d8db73d22..89f02b0049 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphCore.cpp @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + #include "DGTypes.hpp" #include @@ -24,77 +26,60 @@ using namespace DecompGraph::Core; TEST_CASE("Test OperatorNode construction", "[DecompGraph::Core]") { - const OperatorNode op1{"H", 1, 0, false}; - const OperatorNode op2{"CNOT", 2, 0, false}; - const OperatorNode op3{"RX", 1, 1, false}; - const OperatorNode op4{"RZ", 1, 1, true}; - - REQUIRE(op1.name == "H"); - REQUIRE(op1.numWires == 1); - REQUIRE(op1.numParams == 0); - REQUIRE(op1.adjoint == false); - - REQUIRE(op2.name == "CNOT"); - REQUIRE(op2.numWires == 2); - REQUIRE(op2.numParams == 0); - REQUIRE(op2.adjoint == false); - - REQUIRE(op3.name == "RX"); - REQUIRE(op3.numWires == 1); - REQUIRE(op3.numParams == 1); - REQUIRE(op3.adjoint == false); - - REQUIRE(op4.name == "RZ"); - REQUIRE(op4.numWires == 1); - REQUIRE(op4.numParams == 1); - REQUIRE(op4.adjoint == true); + const OperatorNode h{"Hadamard[][1]{}"}; + const OperatorNode cnot{"CNOT[][2]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + + REQUIRE(h.id == "Hadamard[][1]{}"); + REQUIRE(cnot.id == "CNOT[][2]{}"); + REQUIRE(rx.id == "RX[f64][1]{}"); + REQUIRE(rz.id == "RZ[f64][1]{}"); } TEST_CASE("Test OperatorNode equality operator", "[DecompGraph::Core]") { - const OperatorNode op1{"H", 1, 0, false}; - const OperatorNode op2{"H", 1, 0, false}; - const OperatorNode op3{"H", 1, 0, true}; - const OperatorNode op4{"CNOT", 2, 0, false}; - - REQUIRE(op1 == op2); - REQUIRE_FALSE(op1 == op3); - REQUIRE_FALSE(op1 == op4); + const OperatorNode h1{"Hadamard[][1]{}"}; + const OperatorNode h2{"Hadamard[][1]{}"}; + const OperatorNode cnot{"CNOT[][2]{}"}; + + REQUIRE(h1 == h2); + REQUIRE(h1 != cnot); } TEST_CASE("Test OperatorNodeHash", "[DecompGraph::Core]") { - const OperatorNode op1{"H", 1, 0, false}; - const OperatorNode op2{"H", 1, 0, false}; - const OperatorNode op3{"H", 1, 0, true}; - const OperatorNode op4{"CNOT", 2, 0, false}; + const OperatorNode h1{"Hadamard[][1]{}"}; + const OperatorNode h2{"Hadamard[][1]{}"}; + const OperatorNode h3{"Hadamard[][1]{}"}; + const OperatorNode cnot{"CNOT[][2]{}"}; const OperatorNodeHash hashFunc; - REQUIRE(hashFunc(op1) == hashFunc(op2)); - REQUIRE(hashFunc(op1) == hashFunc(op3)); - REQUIRE(hashFunc(op1) != hashFunc(op4)); + REQUIRE(hashFunc(h1) == hashFunc(h2)); + REQUIRE(hashFunc(h1) == hashFunc(h3)); + REQUIRE(hashFunc(h1) != hashFunc(cnot)); } TEST_CASE("Test OperatorNode in unordered_map", "[DecompGraph::Core]") { std::unordered_map opMap; - const OperatorNode op1{"H", 1, 0, false}; - const OperatorNode op2{"CNOT", 2, 0, false}; - const OperatorNode op3{"RX", 1, 1, false}; + const OperatorNode h{"Hadamard[][1]{}"}; + const OperatorNode cnot{"CNOT[][2]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; - opMap[op1] = 1.0; - opMap[op2] = 2.0; + opMap[h] = 1.0; + opMap[cnot] = 2.0; - REQUIRE(opMap[op1] == 1.0); - REQUIRE(opMap[op2] == 2.0); - REQUIRE(opMap.find(op3) == opMap.end()); + REQUIRE(opMap[h] == 1.0); + REQUIRE(opMap[cnot] == 2.0); + REQUIRE(opMap.find(rx) == opMap.end()); } TEST_CASE("Test RuleNode construction", "[DecompGraph::Core]") { - const auto h = OperatorNode{"H"}; - const auto rz = OperatorNode{"RZ"}; - const auto rx = OperatorNode{"RX"}; + const OperatorNode h{"Hadamard[][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; const RuleNode h_to_rz_rx_rz{"h_to_rz_rx_rz", h, {{rz, 2}, {rx, 1}}}; REQUIRE(h_to_rz_rx_rz.name == "h_to_rz_rx_rz"); @@ -108,9 +93,9 @@ TEST_CASE("Test RuleNode construction", "[DecompGraph::Core]") TEST_CASE("Test WeightedGateset construction and contains", "[DecompGraph::Core]") { - const OperatorNode h{"H"}; - const OperatorNode cnot{"CNOT"}; - const OperatorNode rx{"RX"}; + const OperatorNode h{"Hadamard[][1]{}"}; + const OperatorNode cnot{"CNOT[][2]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; const WeightedGateset gateset{{{h, 1.0}, {cnot, 2.0}}}; @@ -124,9 +109,9 @@ TEST_CASE("Test WeightedGateset construction and contains", "[DecompGraph::Core] TEST_CASE("Test ChosenDecompRule construction", "[DecompGraph::Core]") { - const OperatorNode h{"H"}; - const OperatorNode rz{"RZ"}; - const OperatorNode rx{"RX"}; + const OperatorNode h{"Hadamard[][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; const RuleTerm term1{rz, 2}; const RuleTerm term2{rx, 1}; @@ -146,45 +131,3 @@ TEST_CASE("Test ChosenDecompRule construction", "[DecompGraph::Core]") REQUIRE(chosenRule.basisCounts[rz] == 2); REQUIRE(chosenRule.basisCounts[rx] == 1); } - -TEST_CASE("Test graphOpId Support", "[DecompGraph::Core]") -{ - // comparing an op without an ID should fallback to legacy match - const OperatorNode h{"H"}; - const OperatorNode hId{"H", -1, -1, false, {}, "H[][1]{}"}; - - REQUIRE(h == hId); - - // id + legacy match with non-wildcard params - const OperatorNode x{"X", 1, 0}; - const OperatorNode xId{"X", -1, -1, false, {}, "X[][1]{}"}; - - REQUIRE(x == xId); - - // id nodes should fail legacy match if params differ - const OperatorNode op1{"op", 1, 1}; - const OperatorNode op2{"op", 2, 2, false, {}, "op[f64,f64][2]{}"}; - - REQUIRE_FALSE(op1 == op2); - - // nodes with same ids should match - const OperatorNode pr1{"PauliRot", -1, -1, false, {}, "PauliRot[f64][2]{pauli_word:XX}"}; - const OperatorNode pr2{"PauliRot", -1, -1, false, {}, "PauliRot[f64][2]{pauli_word:XX}"}; - - REQUIRE(pr1 == pr2); - - // nodes with matching ids should ignore other parameters (id is source of truth) - const OperatorNode id1{"name1", 1, 1, false, {}, "sameID"}; - const OperatorNode id2{"name2", 2, 2, true, {}, "sameID"}; - - REQUIRE(id1 == id2); - - // Unit test for `OperatorNodeHash`. Check that the hash function prefers ID over name - const OperatorNode hash1{"name", -1, -1, false, {}, "id"}; - const OperatorNode hash2{"name2", 1, 1, true, {}, "id"}; - const OperatorNode hash3{"name2", 1, 1, true, {}}; - - const OperatorNodeHash hashFunc; - REQUIRE(hashFunc(hash1) == hashFunc(hash2)); - REQUIRE(hashFunc(hash2) != hashFunc(hash3)); -} diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolver.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolver.cpp index 46a2602791..2b2a3216eb 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolver.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolver.cpp @@ -13,7 +13,9 @@ // limitations under the License. #include -#include +#include +#include +#include #include "DGBuilder.hpp" #include "DGSolver.hpp" @@ -31,9 +33,9 @@ using namespace DecompGraph::Solver; TEST_CASE("Test DecompositionGraph construction", "[DecompGraph::Solver]") { - const auto h = OperatorNode{"H", 1, 0, false}; - const auto rz = OperatorNode{"RZ", 1, 1, false}; - const auto rx = OperatorNode{"RX", 1, 1, false}; + const auto h = OperatorNode{"H[][1]{}"}; + const auto rz = OperatorNode{"RZ[f64][1]{}"}; + const auto rx = OperatorNode{"RX[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {rx, 2.0}}}; @@ -66,8 +68,8 @@ TEST_CASE("Test DecompositionGraph construction", "[DecompGraph::Solver]") TEST_CASE("Test DecompositionSolver solve method with incomplete gates in Gateset", "[DecompGraph::Solver]") { - const auto h = OperatorNode{"H", 1, 0, false}; - const auto h_gateset = OperatorNode{"H"}; + const auto h = OperatorNode{"H[][1]{}"}; + const auto h_gateset = OperatorNode{"H[][1]{}"}; const WeightedGateset gateset{{{h_gateset, 1.0}}}; const std::vector rules{ {"h_to_h", h, {{h, 1}}}, @@ -91,8 +93,8 @@ TEST_CASE("Test DecompositionSolver solve method with incomplete gates in Gatese TEST_CASE("Do not solve for target gates", "[DecompGraph::Solver]") { - const auto h = OperatorNode{"H", 1, 0, false}; - const auto rz = OperatorNode{"RZ", 1, 1, false}; + const auto h = OperatorNode{"H[][1]{}"}; + const auto rz = OperatorNode{"RZ[f64][1]{}"}; const WeightedGateset gateset{{{h, 2.0}, {rz, 1.0}}}; @@ -112,9 +114,9 @@ TEST_CASE("Do not solve for target gates", "[DecompGraph::Solver]") TEST_CASE("Test DecompositionGraph copy and move semantics", "[DecompGraph::Solver]") { - const auto h = OperatorNode{"H", 1, 0, false}; - const auto rz = OperatorNode{"RZ", 1, 1, false}; - const auto rx = OperatorNode{"RX", 1, 1, false}; + const auto h = OperatorNode{"H[][1]{}"}; + const auto rz = OperatorNode{"RZ[f64][1]{}"}; + const auto rx = OperatorNode{"RX[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {rx, 2.0}}}; @@ -155,10 +157,10 @@ TEST_CASE("Test DecompositionGraph copy and move semantics", "[DecompGraph::Solv TEST_CASE("Test DecompositionGraph lookup and counting", "[DecompGraph::Solver]") { - const OperatorNode h{"H", 1, 0, false}; - const OperatorNode rz{"RZ", 1, 1, false}; - const OperatorNode rx{"RX", 1, 1, false}; - const OperatorNode ry{"RY", 1, 1, false}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; + const OperatorNode ry{"RY[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {ry, 2.0}, {rx, 3.0}}}; @@ -193,13 +195,13 @@ TEST_CASE("Test DecompositionGraph lookup and counting", "[DecompGraph::Solver]" TEST_CASE("Test the graph construction with realistic ops and multiple rules from PennyLane", "[DecompGraph::Solver]") { - const OperatorNode h{"H", 1, 0, false}; - const OperatorNode rz{"RZ", 1, 1, false}; - const OperatorNode rx{"RX", 1, 1, false}; - const OperatorNode ry{"RY", 1, 1, false}; - const OperatorNode cnot{"CNOT", 2, 0, false}; - const OperatorNode swap{"SWAP", 2, 0, false}; - const OperatorNode customBellOp{"BellOp", 2, 0, false}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; + const OperatorNode ry{"RY[f64][1]{}"}; + const OperatorNode cnot{"CNOT[][2]{}"}; + const OperatorNode swap{"SWAP[][2]{}"}; + const OperatorNode customBellOp{"BellOp[][2]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {rx, 3.0}, {cnot, 5.0}}}; @@ -219,10 +221,10 @@ TEST_CASE("Test the graph construction with realistic ops and multiple rules fro TEST_CASE("Test DecompositionSolver with one single operator", "[DecompGraph::Solver]") { - const OperatorNode h{"H", 1, 0, false}; - const OperatorNode rz{"RZ", 1, 1, false}; - const OperatorNode rx{"RX", 1, 1, false}; - const OperatorNode ry{"RY", 1, 1, false}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; + const OperatorNode ry{"RY[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {ry, 2.0}, {rx, 3.0}}}; @@ -258,13 +260,13 @@ TEST_CASE("Test DecompositionSolver with one single operator", "[DecompGraph::So TEST_CASE("Test the graph solver with intermediate ops and multiple rules", "[DecompGraph::Solver]") { - const OperatorNode h{"H", 1, 0, false}; - const OperatorNode rz{"RZ", 1, 1, false}; - const OperatorNode rx{"RX", 1, 1, false}; - const OperatorNode ry{"RY", 1, 1, false}; - const OperatorNode cnot{"CNOT", 2, 0, false}; - const OperatorNode swap{"SWAP", 2, 0, false}; - const OperatorNode customBellOp{"BellOp", 2, 0, false}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; + const OperatorNode ry{"RY[f64][1]{}"}; + const OperatorNode cnot{"CNOT[][2]{}"}; + const OperatorNode swap{"SWAP[][2]{}"}; + const OperatorNode customBellOp{"BellOp[][2]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {rx, 3.0}, {cnot, 5.0}}}; @@ -316,8 +318,8 @@ TEST_CASE("Test the graph solver with intermediate ops and multiple rules", "[De TEST_CASE("Test GraphSolveError for unsolvable operator", "[DecompGraph::Solver]") { - const OperatorNode h{"H", 1, 0, false}; - const OperatorNode rz{"RZ", 1, 1, false}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}}}; @@ -333,7 +335,7 @@ TEST_CASE("Test GraphSolveError for unsolvable operator", "[DecompGraph::Solver] TEST_CASE("Test GraphSolveError for cyclic decomposition", "[DecompGraph::Solver]") { - const OperatorNode h{"H", 1, 0, false}; + const OperatorNode h{"H[][1]{}"}; const WeightedGateset gateset{}; @@ -349,9 +351,9 @@ TEST_CASE("Test GraphSolveError for cyclic decomposition", "[DecompGraph::Solver TEST_CASE("Test PauliX -> GlobalPhase(1), RX(1) decomposition", "[DecompGraph::Solver]") { - const OperatorNode x{"X"}; - const OperatorNode globalPhase{"GlobalPhase"}; - const OperatorNode rx{"RX"}; + const OperatorNode x{"X[][1]{}"}; + const OperatorNode globalPhase{"GlobalPhase[][]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; const WeightedGateset gateset{{{globalPhase, 1.0}, {rx, 1.0}}}; @@ -379,14 +381,14 @@ TEST_CASE("Test PauliX -> GlobalPhase(1), RX(1) decomposition", "[DecompGraph::S TEST_CASE("Test cyclic decomposition with multiple rules for the same operator", "[DecompGraph::Solver]") { - const OperatorNode hadamard{"Hadamard"}; - const OperatorNode globalPhase{"GlobalPhase"}; - const OperatorNode rx{"RX"}; - const OperatorNode rz{"RZ"}; - const OperatorNode ry{"RY"}; - const OperatorNode changeOpBasis{"ChangeOpBasis"}; - const OperatorNode pauliRot{"PauliRot"}; - const OperatorNode rot{"Rot"}; + const OperatorNode hadamard{"Hadamard[][1]{}"}; + const OperatorNode globalPhase{"GlobalPhase[][]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode ry{"RY[f64][1]{}"}; + const OperatorNode changeOpBasis{"ChangeOpBasis[][2]{}"}; + const OperatorNode pauliRot{"PauliRot[f64][2]{pauli_word:XY}"}; + const OperatorNode rot{"Rot[f64,f64,f64][3]{}"}; const std::vector rules{ {"__builtin__ry_to_rz_cliff", ry, {{changeOpBasis, 1}}}, @@ -417,9 +419,9 @@ TEST_CASE("Test cyclic decomposition with multiple rules for the same operator", TEST_CASE("Test GraphBuilder with fixed decomposition", "[DecompGraph::Solver]") { - const OperatorNode h{"H"}; - const OperatorNode rz{"RZ"}; - const OperatorNode rx{"RX"}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {rx, 3.0}}}; @@ -439,9 +441,9 @@ TEST_CASE("Test GraphBuilder with fixed decomposition", "[DecompGraph::Solver]") TEST_CASE("Test GraphBuilder with alternative decomposition", "[DecompGraph::Solver]") { - const OperatorNode h{"H"}; - const OperatorNode rz{"RZ"}; - const OperatorNode rx{"RX"}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {rx, 3.0}}}; @@ -459,9 +461,9 @@ TEST_CASE("Test GraphBuilder with alternative decomposition", "[DecompGraph::Sol TEST_CASE("Test GraphSolver with fixed decomposition", "[DecompGraph::Solver]") { - const OperatorNode h{"H"}; - const OperatorNode rz{"RZ"}; - const OperatorNode rx{"RX"}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; const WeightedGateset gateset{{{rz, 3.0}, {rx, 1.0}}}; @@ -485,9 +487,9 @@ TEST_CASE("Test GraphSolver with fixed decomposition", "[DecompGraph::Solver]") TEST_CASE("Test GraphSolver with alternative decomposition", "[DecompGraph::Solver]") { - const OperatorNode h{"H"}; - const OperatorNode rz{"RZ"}; - const OperatorNode rx{"RX"}; + const OperatorNode h{"H[][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode rx{"RX[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {rx, 3.0}}}; @@ -510,9 +512,9 @@ TEST_CASE("Test GraphSolver with alternative decomposition", "[DecompGraph::Solv TEST_CASE("Test GraphSolver with MultiRZ decompositions", "[DecompGraph::Solver]") { - const OperatorNode multiRZ3{"MultiRZ3"}; - const OperatorNode multiRZ5{"MultiRZ5"}; - const OperatorNode rz{"RZ"}; + const OperatorNode multiRZ3{"MultiRZ[f64][3]{}"}; + const OperatorNode multiRZ5{"MultiRZ[f64][5]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}}}; @@ -537,8 +539,8 @@ TEST_CASE("Test GraphSolver with MultiRZ decompositions", "[DecompGraph::Solver] TEST_CASE("Test GraphSolver with empty decomposition rules", "[DecompGraph::Solver]") { - const OperatorNode hadamard{"Hadamard"}; - const OperatorNode globalPhase{"GlobalPhase"}; + const OperatorNode hadamard{"Hadamard[][1]{}"}; + const OperatorNode globalPhase{"GlobalPhase[][]{}"}; const WeightedGateset gateset{{{globalPhase, 1.0}}}; @@ -557,47 +559,12 @@ TEST_CASE("Test GraphSolver with empty decomposition rules", "[DecompGraph::Solv REQUIRE(chosen_rule.totalCost == 0.0); } -TEST_CASE("Test GraphSolver with PauliRot specialized by static argument pauli_word", - "[DecompGraph::Solver]") -{ - // Query: PauliRot[w:1][p:1][pauli_word:X] should match a rule whose output is - // PauliRot[w:-1][p:-1][pauli_word:X] (wildcards on wires/params, exact match on pauli_word). - const OperatorNode pauliRotQuery{"PauliRot", 1, 1, false, {{"pauli_word", "X"}}}; - const OperatorNode pauliRotRuleOutput{"PauliRot", -1, -1, false, {{"pauli_word", "X"}}}; - const OperatorNode hadamard{"Hadamard", 1, 0, false}; - const OperatorNode multiRZ{"MultiRZ", 1, 1, false}; - - const WeightedGateset gateset{{{hadamard, 1.0}, {multiRZ, 1.0}}}; - - const std::vector rules{ - {"_pauli_rot_decomposition_X", pauliRotRuleOutput, {{hadamard, 2}, {multiRZ, 1}}}, - }; - - const DecompositionGraph graph({pauliRotQuery}, gateset, rules); - DecompositionSolver solver(graph); - const auto result = solver.solve(); - - REQUIRE(result.find(pauliRotQuery) != result.end()); - const auto &chosen = result.at(pauliRotQuery); - REQUIRE_FALSE(chosen.isBasis); - REQUIRE(chosen.ruleName == "_pauli_rot_decomposition_X"); - REQUIRE(chosen.totalCost == 1.0 * 2 + 1.0 * 1); - REQUIRE(chosen.basisCounts.at(hadamard) == 2); - REQUIRE(chosen.basisCounts.at(multiRZ) == 1); - - const OperatorNode pauliRotQueryY{"PauliRot", 1, 1, false, {{"pauli_word", "Y"}}}; - REQUIRE_FALSE(pauliRotQuery == pauliRotQueryY); - REQUIRE(pauliRotQuery == pauliRotRuleOutput); -} - TEST_CASE("Test OperatorNode equality with staticNamedArgs", "[DecompGraph::Core]") { - const OperatorNode pauliRotX{"PauliRot", 1, 1, false, {{"pauli_word", "X"}}}; - const OperatorNode pauliRotXWildcard{"PauliRot", -1, -1, false, {{"pauli_word", "X"}}}; - const OperatorNode pauliRotY{"PauliRot", 1, 1, false, {{"pauli_word", "Y"}}}; - const OperatorNode pauliRotNoArgs{"PauliRot", 1, 1, false}; + const OperatorNode pauliRotX{"PauliRot[f64][1]{pauli_word:X}"}; + const OperatorNode pauliRotX2{"PauliRot[f64][1]{pauli_word:X}"}; + const OperatorNode pauliRotY{"PauliRot[f64][1]{pauli_word:Y}"}; - REQUIRE(pauliRotX == pauliRotXWildcard); + REQUIRE(pauliRotX == pauliRotX2); REQUIRE_FALSE(pauliRotX == pauliRotY); - REQUIRE(pauliRotX == pauliRotNoArgs); } From 6fe3d57ddedc28813e9cba0f635ad5e04d989e17 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:22:49 -0400 Subject: [PATCH 3/8] Refactor and tidy up the Adjoint op support in the graph solver (git:aa/adjoint-op) --- .../DecompGraphSolver/DGBuilder.cpp | 74 +++++++++++++++++++ .../DecompGraphSolver/DGSolver.cpp | 1 + .../DecompGraphSolver/DGTypes.hpp | 38 +++++++++- .../DecompGraphSolver/CMakeLists.txt | 1 + 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp index 97187aa0a3..80183a4a94 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp @@ -111,6 +111,7 @@ struct DecompositionGraph::Impl { fixedDecomps(std::move(_fixedDecomps)), altDecomps(std::move(_altDecomps)) { materializeRules(); + generateAdjointRules(); } void materializeRules() @@ -166,6 +167,79 @@ struct DecompositionGraph::Impl { rules = std::move(effectiveRules); } + /** + * @brief Generate Adjoint decomposition rules + * + * For a needed adjoint operator `Adjoint(Op)` this synthesizes, from every base decomposition + * `Op`, a rule `Adjoint(Op)` so the adjoint operator can be decomposed by adjointing the + * decomposition of its base. These coexist with any explicitly registered adjoint rules; + * the solver then compares their costs and picks the cheapest. + * + * @note Rules are synthesized only on-demand: only Adjoint operators that actually appear + * in the circuit (as roots) seed the process, and adjointing a decomposition may introduce + * new Adjoint(input) operators that require their own synthesized rules, so the process + * runs to a fixpoint. We leave the graph untouched when there aren't any adjoint ops. + * + * Two families of rules are never used as a base in this method: + * - rules whose output is already adjoint: we only derive adjoint rules from base + * decompositions. + * - empty rules in the graph: these mark basis/target gates, and the adjoint of + * a basis gate is not necessarily available for free, so it must be provided + * explicitly (e.g. a self_adjoint rule) rather than synthesized. + * TODO: we'll revisit this when integrating this with graph-decomposition. + */ + void generateAdjointRules() + { + // Index valid base decompositions by their (non-adjoint) output. + std::unordered_map, OperatorNodeHash> baseByOutput; + for (const auto &rule : rules) { + if (!rule.output.adjoint && !rule.isEmpty()) { + baseByOutput[rule.output].push_back(rule); + } + } + if (baseByOutput.empty()) { + return; + } + + // for every Adjoint(Op) in the circuit: + std::unordered_set seen; + std::vector worklist; + auto enqueue = [&](const OperatorNode &op) { + if (op.adjoint && seen.insert(op).second) { + worklist.push_back(op); + } + }; + for (const auto &op : operators) { + enqueue(op); + } + for (const auto &rule : rules) { + enqueue(rule.output); + for (const auto &term : rule.inputs) { + enqueue(term.op); + } + } + + std::vector generated; + while (!worklist.empty()) { + const OperatorNode adjOp = worklist.back(); + worklist.pop_back(); + const auto it = baseByOutput.find(makeAdjoint(adjOp)); + if (it == baseByOutput.end()) { + continue; + } + for (const auto &baseRule : it->second) { + RuleNode adjRule = makeAdjointRule(baseRule); + for (const auto &term : adjRule.inputs) { + enqueue(term.op); + } + generated.push_back(std::move(adjRule)); + } + } + for (auto &rule : generated) { + rules.push_back(std::move(rule)); + } + } + void buildGraph() { // Register all operators diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.cpp index 35b7d760ae..effacc9723 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.cpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGSolver.cpp @@ -53,6 +53,7 @@ ChosenDecompRule DecompositionSolver::evalRule(const RuleNode &rule) solution.isBasis = false; solution.inputs = rule.inputs; solution.op = rule.output; + solution.origin = rule.origin; double total_cost = 0.0; for (const auto &input : rule.inputs) { diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp index cd4aaeb844..46e229b621 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp @@ -126,8 +126,9 @@ struct RuleTerm { * graph. * - Fixed: A fixed rule that cannot be changed or overridden by the solver. * - Alternative: An alternative rule that can be used in place of the default rule. + * - AdjointGenerated: A rule synthesized by adjointing a base decomposition rule. */ -enum class RuleOrigin : uint8_t { Default = 0, Fixed = 1, Alternative = 2 }; +enum class RuleOrigin : uint8_t { Default = 0, Fixed = 1, Alternative = 2, AdjointGenerated = 3 }; /** * @brief This represents the decomposition rules in the graph decomposition problem. @@ -172,6 +173,38 @@ using FixedDecomps = std::unordered_map, OperatorNodeHash>; +/** + * @brief This returns a copy of the given operator with its adjoint flag. + * + * Note taht because Adjoint is represented as a boolean flag in the IR, + * applying it twice should cancel out (Adjoint(Adjoint(op)) == op). + */ +inline OperatorNode makeAdjoint(OperatorNode op) +{ + op.adjoint = !op.adjoint; + return op; +} + +/** + * @brief Constructs the Adjoint decomposition of a base rule. + * + * Given a rule `output -> {inputs}`, produces `Adjoint(output) -> {Adjoint(input), ...}` + * with the same multiplicities: the adjoint of a decomposition is obtained by adjointing + * every produced gate (and reversing their order, which does not affect resource/cost counting). + */ +inline RuleNode makeAdjointRule(const RuleNode &base) +{ + RuleNode adj; + adj.name = base.name + "_adjoint"; + adj.output = makeAdjoint(base.output); + adj.origin = RuleOrigin::AdjointGenerated; + adj.inputs.reserve(base.inputs.size()); + for (const auto &term : base.inputs) { + adj.inputs.push_back({makeAdjoint(term.op), term.multiplicity}); + } + return adj; +} + /** * @brief This represents the chosen decomposition rule for an operator in * the solution of the graph decomposition problem. @@ -183,6 +216,9 @@ struct ChosenDecompRule { std::vector inputs; double totalCost{0.0}; std::unordered_map basisCounts; + + // TODO: revisit this after testing.. + RuleOrigin origin{RuleOrigin::Default}; }; /** diff --git a/mlir/unittests/DecompGraphSolver/CMakeLists.txt b/mlir/unittests/DecompGraphSolver/CMakeLists.txt index afa4b9e197..43969b26d6 100644 --- a/mlir/unittests/DecompGraphSolver/CMakeLists.txt +++ b/mlir/unittests/DecompGraphSolver/CMakeLists.txt @@ -25,6 +25,7 @@ include(Catch) add_executable(runner_tests_dgsolver Test_DecompGraphCore.cpp Test_DecompGraphSolver.cpp + Test_DecompGraphSolverSymbolicOps.cpp ) target_link_libraries(runner_tests_dgsolver PRIVATE From 424a95878519547ba65c5d72381891044322db33 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:23:13 -0400 Subject: [PATCH 4/8] Add the C++ test file --- .../Test_DecompGraphSolverSymbolicOps.cpp | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp new file mode 100644 index 0000000000..72d9a82bd2 --- /dev/null +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp @@ -0,0 +1,66 @@ +// Copyright 2026 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "DGBuilder.hpp" +#include "DGSolver.hpp" +#include "DGTypes.hpp" +#include "DGUtils.hpp" + +#include +#include +#include +#include + +using namespace Catch::Matchers; +using namespace DecompGraph::Core; +using namespace DecompGraph::Solver; + +TEST_CASE("Test makeAdjoint and cancels on double application", + "[DecompGraph::Core]") +{ + const OperatorNode h{"H", 1, 0, false}; + const OperatorNode adjH = makeAdjoint(h); + + REQUIRE(adjH.name == "H"); + REQUIRE(adjH.adjoint); + REQUIRE(adjH != h); + + // cancel_adjoint: Adjoint(Adjoint(H)) == H + REQUIRE(makeAdjoint(adjH) == h); + REQUIRE_FALSE(makeAdjoint(adjH).adjoint); +} + +TEST_CASE("Test makeAdjointRule", "[DecompGraph::Core]") +{ + const OperatorNode rot{"Rot", 1, 3, false}; + const OperatorNode rz{"RZ", 1, 1, false}; + const OperatorNode ry{"RY", 1, 1, false}; + const RuleNode base{"rot_decomp", rot, {{rz, 2}, {ry, 1}}}; + + const RuleNode adj = makeAdjointRule(base); + + REQUIRE(adj.name == "rot_decomp_adjoint"); + REQUIRE(adj.origin == RuleOrigin::AdjointGenerated); + REQUIRE(adj.output == makeAdjoint(rot)); + REQUIRE(adj.output.adjoint); + REQUIRE(adj.inputs.size() == 2); + REQUIRE(adj.inputs[0].op == makeAdjoint(rz)); + REQUIRE(adj.inputs[0].op.adjoint); + REQUIRE(adj.inputs[0].multiplicity == 2); + REQUIRE(adj.inputs[1].op == makeAdjoint(ry)); + REQUIRE(adj.inputs[1].multiplicity == 1); +} From 7ce9c48492cce47801a194c79c3e4fd2a3299580 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:41:12 -0400 Subject: [PATCH 5/8] Update format --- .../DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp index 72d9a82bd2..3b84666f24 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp @@ -29,8 +29,7 @@ using namespace Catch::Matchers; using namespace DecompGraph::Core; using namespace DecompGraph::Solver; -TEST_CASE("Test makeAdjoint and cancels on double application", - "[DecompGraph::Core]") +TEST_CASE("Test makeAdjoint and cancels on double application", "[DecompGraph::Core]") { const OperatorNode h{"H", 1, 0, false}; const OperatorNode adjH = makeAdjoint(h); From fb5ac4716d9f9cb1a9e2987f772173444f7ea7c9 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:47:55 -0400 Subject: [PATCH 6/8] Update tests --- .../Test_DecompGraphSolverSymbolicOps.cpp | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp index 3b84666f24..51ce4ca22b 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp @@ -63,3 +63,166 @@ TEST_CASE("Test makeAdjointRule", "[DecompGraph::Core]") REQUIRE(adj.inputs[1].op == makeAdjoint(ry)); REQUIRE(adj.inputs[1].multiplicity == 1); } + +TEST_CASE("Test DecompositionGraph adjoint rules from base rules", + "[DecompGraph::Solver]") +{ + const OperatorNode rot{"Rot", 1, 3, false}; + const OperatorNode rz{"RZ", 1, 1, false}; + const OperatorNode ry{"RY", 1, 1, false}; + + const WeightedGateset gateset{{{rz, 1.0}, {ry, 1.0}}}; + const std::vector rules{{"rot_decomp", rot, {{rz, 2}, {ry, 1}}}}; + + // Adjoint(Rot) is a root, so the builder should synthesize its adjoint decomposition + const DecompositionGraph graph({makeAdjoint(rot)}, gateset, rules); + + REQUIRE(graph.getNumRules() == 2); + REQUIRE(graph.hasOperator(makeAdjoint(rot))); + + const auto &adjRules = graph.getAllRulesFor(makeAdjoint(rot)); + REQUIRE(adjRules.size() == 1); + REQUIRE(adjRules[0].name == "rot_decomp_adjoint"); + REQUIRE(adjRules[0].origin == RuleOrigin::AdjointGenerated); + REQUIRE(adjRules[0].output == makeAdjoint(rot)); + REQUIRE(adjRules[0].inputs[0].op == makeAdjoint(rz)); + REQUIRE(adjRules[0].inputs[1].op == makeAdjoint(ry)); +} + +TEST_CASE("Test DecompositionGraph does not synthesize adjoint rules for empty or adjoint rules", + "[DecompGraph::Solver]") +{ + const OperatorNode h{"H", 1, 0, false}; + const OperatorNode adjH = makeAdjoint(h); + + const WeightedGateset gateset{{{h, 1.0}}}; + const std::vector rules{ + {"h_is_basis", h, {}}, // empty rule + {"self_adjoint_H", adjH, {{h, 1}}}, // adjoint output, must not be mirrored!! + }; + + const DecompositionGraph graph({h}, gateset, rules); + + REQUIRE(graph.getNumRules() == 2); + REQUIRE(graph.getAllRulesFor(adjH).size() == 1); + REQUIRE(graph.getAllRulesFor(adjH)[0].name == "self_adjoint_H"); +} + +TEST_CASE("Test Adjoint: self_adjoint (Adjoint(H) -> H)", "[DecompGraph::Solver]") +{ + const OperatorNode h{"H", 1, 0, false}; + const OperatorNode adjH = makeAdjoint(h); + + const WeightedGateset gateset{{{h, 1.0}}}; + const std::vector rules{{"self_adjoint_H", adjH, {{h, 1}}}}; + + const DecompositionGraph graph({adjH}, gateset, rules); + DecompositionSolver solver(graph); + const auto result = solver.solve(); + + REQUIRE(result.find(adjH) != result.end()); + const auto &chosen = result.at(adjH); + REQUIRE_FALSE(chosen.isBasis); + REQUIRE(chosen.ruleName == "self_adjoint_H"); + REQUIRE(chosen.origin == RuleOrigin::Default); + REQUIRE(chosen.totalCost == 1.0); + REQUIRE(chosen.basisCounts.at(h) == 1); + + REQUIRE(graph.getAllRulesFor(adjH).size() == 1); +} + +TEST_CASE("Test Adjoint: adjoint_rotation (Adjoint(RX) -> RX)", "[DecompGraph::Solver]") +{ + const OperatorNode rx{"RX", 1, 1, false}; + const OperatorNode adjRX = makeAdjoint(rx); + + const WeightedGateset gateset{{{rx, 1.0}}}; + const std::vector rules{{"adjoint_rotation_RX", adjRX, {{rx, 1}}}}; + + const DecompositionGraph graph({adjRX}, gateset, rules); + DecompositionSolver solver(graph); + const auto result = solver.solve(); + + const auto &chosen = result.at(adjRX); + REQUIRE(chosen.ruleName == "adjoint_rotation_RX"); + REQUIRE(chosen.totalCost == 1.0); + REQUIRE(chosen.basisCounts.at(rx) == 1); +} + +TEST_CASE("Test Adjoint: multiple rules and the solver should pick the cheapest", "[DecompGraph::Solver]") +{ + const OperatorNode rot{"Rot", 1, 3, false}; + const OperatorNode rz{"RZ", 1, 1, false}; + const OperatorNode ry{"RY", 1, 1, false}; + const OperatorNode e{"E", 1, 0, false}; + + const std::vector commonRules{ + {"rot_decomp", rot, {{rz, 2}, {ry, 1}}}, + {"adjoint_rotation_RZ", makeAdjoint(rz), {{rz, 1}}}, + {"adjoint_rotation_RY", makeAdjoint(ry), {{ry, 1}}}, + }; + + SECTION("rot_decomp_adjoint is cheaper") + { + const WeightedGateset gateset{{{rz, 1.0}, {ry, 1.0}, {e, 10.0}}}; + std::vector rules = commonRules; + rules.push_back({"_adjoint_rot", makeAdjoint(rot), {{e, 1}}}); // cost 10 + + const DecompositionGraph graph({makeAdjoint(rot)}, gateset, rules); + + // Both an explicit adjoint rule and the synthesized one exist for Adjoint(Rot). + REQUIRE(graph.getAllRulesFor(makeAdjoint(rot)).size() == 2); + + DecompositionSolver solver(graph); + const auto result = solver.solve(); + const auto &chosen = result.at(makeAdjoint(rot)); + REQUIRE(chosen.ruleName == "rot_decomp_adjoint"); + REQUIRE(chosen.origin == RuleOrigin::AdjointGenerated); + REQUIRE(chosen.totalCost == 3.0); + REQUIRE(chosen.basisCounts.at(rz) == 2); + REQUIRE(chosen.basisCounts.at(ry) == 1); + } + + SECTION("_adjoint_rot is cheaper") + { + const WeightedGateset gateset{{{rz, 1.0}, {ry, 1.0}}}; + std::vector rules = commonRules; + rules.push_back({"_adjoint_rot", makeAdjoint(rot), {{rz, 1}}}); // cost 1 + + const DecompositionGraph graph({makeAdjoint(rot)}, gateset, rules); + DecompositionSolver solver(graph); + const auto result = solver.solve(); + const auto &chosen = result.at(makeAdjoint(rot)); + REQUIRE(chosen.ruleName == "_adjoint_rot"); + REQUIRE(chosen.origin == RuleOrigin::Default); + REQUIRE(chosen.totalCost == 1.0); + } +} + +TEST_CASE("Test Adjoint: adjoint pushed through a decomposition", "[DecompGraph::Solver]") +{ + const OperatorNode myOp{"MyOp", 2, 0, false}; + const OperatorNode a{"A", 1, 0, false}; + const OperatorNode b{"B", 1, 0, false}; + + const WeightedGateset gateset{{{a, 1.0}, {b, 1.0}}}; + const std::vector rules{ + {"myop_decomp", myOp, {{a, 1}, {b, 1}}}, + // Define self_adjoint rules so the adjointed produced gates can resolve: + {"self_adjoint_A", makeAdjoint(a), {{a, 1}}}, + {"self_adjoint_B", makeAdjoint(b), {{b, 1}}}, + }; + + const DecompositionGraph graph({makeAdjoint(myOp)}, gateset, rules); + + REQUIRE(graph.getAllRulesFor(makeAdjoint(myOp)).size() == 1); + + DecompositionSolver solver(graph); + const auto result = solver.solve(); + const auto &chosen = result.at(makeAdjoint(myOp)); + REQUIRE(chosen.ruleName == "myop_decomp_adjoint"); + REQUIRE(chosen.origin == RuleOrigin::AdjointGenerated); + REQUIRE(chosen.totalCost == 2.0); + REQUIRE(chosen.basisCounts.at(a) == 1); + REQUIRE(chosen.basisCounts.at(b) == 1); +} From 44cdf32e989184acbbb24cb6eb64fb8c769c78a3 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:50:33 -0400 Subject: [PATCH 7/8] Update --- doc/releases/changelog-dev.md | 5 +++++ .../Test_DecompGraphSolverSymbolicOps.cpp | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 589bba71f6..56f6458fd0 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -12,6 +12,11 @@

Improvements 🛠

+* Add adjoint support to the decomposition graph solver, enabling `Adjoint(Op)` to be decomposed either + via registered adjoint rules or by adjointing the base operator's decomposition rule, + with the solver choosing the cheapest. + [(#3001)](https://github.com/PennyLaneAI/catalyst/pull/3001) + * The `ResourceAnalysis` pass has received a new compiler hint to more accurately estimate quantum resources in the presence of conditional operations (`scf.if` and `scf.index_switch`). The operations in question can be annotated with either a `catalyst.estimated_probability` or diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp index 51ce4ca22b..26b09e5b27 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp @@ -64,8 +64,7 @@ TEST_CASE("Test makeAdjointRule", "[DecompGraph::Core]") REQUIRE(adj.inputs[1].multiplicity == 1); } -TEST_CASE("Test DecompositionGraph adjoint rules from base rules", - "[DecompGraph::Solver]") +TEST_CASE("Test DecompositionGraph adjoint rules from base rules", "[DecompGraph::Solver]") { const OperatorNode rot{"Rot", 1, 3, false}; const OperatorNode rz{"RZ", 1, 1, false}; @@ -97,7 +96,7 @@ TEST_CASE("Test DecompositionGraph does not synthesize adjoint rules for empty o const WeightedGateset gateset{{{h, 1.0}}}; const std::vector rules{ - {"h_is_basis", h, {}}, // empty rule + {"h_is_basis", h, {}}, // empty rule {"self_adjoint_H", adjH, {{h, 1}}}, // adjoint output, must not be mirrored!! }; @@ -149,7 +148,8 @@ TEST_CASE("Test Adjoint: adjoint_rotation (Adjoint(RX) -> RX)", "[DecompGraph::S REQUIRE(chosen.basisCounts.at(rx) == 1); } -TEST_CASE("Test Adjoint: multiple rules and the solver should pick the cheapest", "[DecompGraph::Solver]") +TEST_CASE("Test Adjoint: multiple rules and the solver should pick the cheapest", + "[DecompGraph::Solver]") { const OperatorNode rot{"Rot", 1, 3, false}; const OperatorNode rz{"RZ", 1, 1, false}; From 4100123304fab1ef7316167d73a9d2bb0841b034 Mon Sep 17 00:00:00 2001 From: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:51:14 -0400 Subject: [PATCH 8/8] Rebase to pass-id-support --- .../DecompGraphSolver/DGTypes.hpp | 24 ++++++++++--- .../Test_DecompGraphSolverSymbolicOps.cpp | 36 +++++++++---------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp index 46e229b621..8a311758e4 100644 --- a/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp +++ b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGTypes.hpp @@ -174,14 +174,30 @@ using FixedDecomps = std::unordered_map, OperatorNodeHash>; /** - * @brief This returns a copy of the given operator with its adjoint flag. + * @brief This returns a copy of the given operator with the adjoint modifier toggled. * - * Note taht because Adjoint is represented as a boolean flag in the IR, - * applying it twice should cancel out (Adjoint(Adjoint(op)) == op). + * Identity is the opaque `id` string (equality/hashing are id-only), + * so the modifier must be folded into the id: we wrap it in `Adjoint(...)` + * (or strip that wrapper to cancel adjoint). + * Applying twice cancels: `makeAdjoint(makeAdjoint(op)) == op`. */ inline OperatorNode makeAdjoint(OperatorNode op) { - op.adjoint = !op.adjoint; + static constexpr char kPrefix[] = "Adjoint("; + constexpr std::size_t kPrefixLen = sizeof(kPrefix) - 1; + + if (op.adjoint) { + // Cancel: strip the outermost "Adjoint( ... )" wrapper from the id. + if (op.id.size() > kPrefixLen && op.id.compare(0, kPrefixLen, kPrefix) == 0 && + op.id.back() == ')') { + op.id = op.id.substr(kPrefixLen, op.id.size() - kPrefixLen - 1); + } + op.adjoint = false; + } + else { + op.id = std::string(kPrefix) + op.id + ")"; + op.adjoint = true; + } return op; } diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp index 26b09e5b27..76f7783980 100644 --- a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp +++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp @@ -31,10 +31,10 @@ using namespace DecompGraph::Solver; TEST_CASE("Test makeAdjoint and cancels on double application", "[DecompGraph::Core]") { - const OperatorNode h{"H", 1, 0, false}; + const OperatorNode h{"H[][1]{}"}; const OperatorNode adjH = makeAdjoint(h); - REQUIRE(adjH.name == "H"); + REQUIRE(adjH.id == "Adjoint(H[][1]{})"); REQUIRE(adjH.adjoint); REQUIRE(adjH != h); @@ -45,9 +45,9 @@ TEST_CASE("Test makeAdjoint and cancels on double application", "[DecompGraph::C TEST_CASE("Test makeAdjointRule", "[DecompGraph::Core]") { - const OperatorNode rot{"Rot", 1, 3, false}; - const OperatorNode rz{"RZ", 1, 1, false}; - const OperatorNode ry{"RY", 1, 1, false}; + const OperatorNode rot{"Rot[f64,f64,f64][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode ry{"RY[f64][1]{}"}; const RuleNode base{"rot_decomp", rot, {{rz, 2}, {ry, 1}}}; const RuleNode adj = makeAdjointRule(base); @@ -66,9 +66,9 @@ TEST_CASE("Test makeAdjointRule", "[DecompGraph::Core]") TEST_CASE("Test DecompositionGraph adjoint rules from base rules", "[DecompGraph::Solver]") { - const OperatorNode rot{"Rot", 1, 3, false}; - const OperatorNode rz{"RZ", 1, 1, false}; - const OperatorNode ry{"RY", 1, 1, false}; + const OperatorNode rot{"Rot[f64,f64,f64][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode ry{"RY[f64][1]{}"}; const WeightedGateset gateset{{{rz, 1.0}, {ry, 1.0}}}; const std::vector rules{{"rot_decomp", rot, {{rz, 2}, {ry, 1}}}}; @@ -91,7 +91,7 @@ TEST_CASE("Test DecompositionGraph adjoint rules from base rules", "[DecompGraph TEST_CASE("Test DecompositionGraph does not synthesize adjoint rules for empty or adjoint rules", "[DecompGraph::Solver]") { - const OperatorNode h{"H", 1, 0, false}; + const OperatorNode h{"H[][1]{}"}; const OperatorNode adjH = makeAdjoint(h); const WeightedGateset gateset{{{h, 1.0}}}; @@ -109,7 +109,7 @@ TEST_CASE("Test DecompositionGraph does not synthesize adjoint rules for empty o TEST_CASE("Test Adjoint: self_adjoint (Adjoint(H) -> H)", "[DecompGraph::Solver]") { - const OperatorNode h{"H", 1, 0, false}; + const OperatorNode h{"H[][1]{}"}; const OperatorNode adjH = makeAdjoint(h); const WeightedGateset gateset{{{h, 1.0}}}; @@ -132,7 +132,7 @@ TEST_CASE("Test Adjoint: self_adjoint (Adjoint(H) -> H)", "[DecompGraph::Solver] TEST_CASE("Test Adjoint: adjoint_rotation (Adjoint(RX) -> RX)", "[DecompGraph::Solver]") { - const OperatorNode rx{"RX", 1, 1, false}; + const OperatorNode rx{"RX[f64][1]{}"}; const OperatorNode adjRX = makeAdjoint(rx); const WeightedGateset gateset{{{rx, 1.0}}}; @@ -151,10 +151,10 @@ TEST_CASE("Test Adjoint: adjoint_rotation (Adjoint(RX) -> RX)", "[DecompGraph::S TEST_CASE("Test Adjoint: multiple rules and the solver should pick the cheapest", "[DecompGraph::Solver]") { - const OperatorNode rot{"Rot", 1, 3, false}; - const OperatorNode rz{"RZ", 1, 1, false}; - const OperatorNode ry{"RY", 1, 1, false}; - const OperatorNode e{"E", 1, 0, false}; + const OperatorNode rot{"Rot[f64,f64,f64][1]{}"}; + const OperatorNode rz{"RZ[f64][1]{}"}; + const OperatorNode ry{"RY[f64][1]{}"}; + const OperatorNode e{"E[][1]{}"}; const std::vector commonRules{ {"rot_decomp", rot, {{rz, 2}, {ry, 1}}}, @@ -201,9 +201,9 @@ TEST_CASE("Test Adjoint: multiple rules and the solver should pick the cheapest" TEST_CASE("Test Adjoint: adjoint pushed through a decomposition", "[DecompGraph::Solver]") { - const OperatorNode myOp{"MyOp", 2, 0, false}; - const OperatorNode a{"A", 1, 0, false}; - const OperatorNode b{"B", 1, 0, false}; + const OperatorNode myOp{"MyOp[][2]{}"}; + const OperatorNode a{"A[][1]{}"}; + const OperatorNode b{"B[][1]{}"}; const WeightedGateset gateset{{{a, 1.0}, {b, 1.0}}}; const std::vector rules{