diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index f6ac0cf325..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
@@ -313,6 +318,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/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/DecompGraphSolver/DGBuilder.cpp b/mlir/lib/Quantum/Transforms/GraphDecomposition/DecompGraphSolver/DGBuilder.cpp
index 7d2724941c..80183a4a94 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;
@@ -101,6 +111,7 @@ struct DecompositionGraph::Impl {
fixedDecomps(std::move(_fixedDecomps)), altDecomps(std::move(_altDecomps))
{
materializeRules();
+ generateAdjointRules();
}
void materializeRules()
@@ -156,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/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..effacc9723 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;
@@ -51,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/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..8a311758e4 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();
}
};
@@ -170,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.
@@ -216,6 +173,54 @@ using FixedDecomps = std::unordered_map, OperatorNodeHash>;
+/**
+ * @brief This returns a copy of the given operator with the adjoint modifier toggled.
+ *
+ * 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)
+{
+ 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;
+}
+
+/**
+ * @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.
@@ -227,6 +232,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/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 39929153a4..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())) {
@@ -476,6 +464,10 @@ struct GraphDecompositionPass : public impl::GraphDecompositionPassBase
+
#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);
}
diff --git a/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp
new file mode 100644
index 0000000000..76f7783980
--- /dev/null
+++ b/mlir/unittests/DecompGraphSolver/Test_DecompGraphSolverSymbolicOps.cpp
@@ -0,0 +1,228 @@
+// 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]{}"};
+ const OperatorNode adjH = makeAdjoint(h);
+
+ REQUIRE(adjH.id == "Adjoint(H[][1]{})");
+ 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[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);
+
+ 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);
+}
+
+TEST_CASE("Test DecompositionGraph adjoint rules from base rules", "[DecompGraph::Solver]")
+{
+ 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}}}};
+
+ // 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]{}"};
+ 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]{}"};
+ 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[f64][1]{}"};
+ 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[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}}},
+ {"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]{}"};
+ const OperatorNode a{"A[][1]{}"};
+ const OperatorNode b{"B[][1]{}"};
+
+ 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);
+}