From 3ca8f0de183d03911bdadcad0fbb2fbe45a7e26e 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 1/5] Refactor and tidy up the Adjoint op support in the graph solver (git:aa/adjoint-op) --- .../DecompGraphSolver/DGBuilder.cpp | 74 +++++++++++++++++++ .../Transforms/DecompGraphSolver/DGSolver.cpp | 1 + .../Transforms/DecompGraphSolver/DGTypes.hpp | 38 +++++++++- .../DecompGraphSolver/CMakeLists.txt | 1 + 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGBuilder.cpp b/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGBuilder.cpp index 894cd45d83..aa9ba60a4a 100644 --- a/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGBuilder.cpp +++ b/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGBuilder.cpp @@ -100,6 +100,7 @@ struct DecompositionGraph::Impl { fixedDecomps(std::move(_fixedDecomps)), altDecomps(std::move(_altDecomps)) { materializeRules(); + generateAdjointRules(); } void materializeRules() @@ -155,6 +156,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/DecompGraphSolver/DGSolver.cpp b/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGSolver.cpp index c83ccddce5..d05fcb321a 100644 --- a/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGSolver.cpp +++ b/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGSolver.cpp @@ -51,6 +51,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/DecompGraphSolver/DGTypes.hpp b/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGTypes.hpp index 9734285415..dc80dfb58e 100644 --- a/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGTypes.hpp +++ b/mlir/lib/Quantum/Transforms/DecompGraphSolver/DGTypes.hpp @@ -140,8 +140,9 @@ struct RuleTerm { * - Default: The default rule for decomposing an operator as defined in the decomposition 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. @@ -185,6 +186,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. @@ -196,6 +229,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 384a3cb7cf..168d192b8d 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 437dd22c98efcf5f5c37d7d22f847430df34c1ee 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 2/5] 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 252196140618c9cbaef7a76ced8e25dbb4c94a0d 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 3/5] 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 c0f3ca6324ad4621b797dbc3faa816b3eb687a8b 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 4/5] 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 461db1ea93029976412cade498b7144b82f0b0a1 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 5/5] 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 fd4a8fe2bc..edb2254e02 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -5,6 +5,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 `decompose-lowering` pass now supports applying a selection of the available decomposition rules via the `target_rules` parameter. The pass also no longer applies the `inline`, `cse` and `canonicalize` passes to avoid unnecessary IR mutations. Instead, decomposition rules are deterministically inlined by a custom function (`inline` is non-deterministic, using an estimated benefit and threshold as criteria for inlining). 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};