-
Notifications
You must be signed in to change notification settings - Fork 83
Support decomposition to/from Adjoint Ops to the GraphSolver #3001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ce8ab1d
0ff678b
6fe3d57
424a958
7ce9c48
fb5ac47
44cdf32
4100123
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
|
|
||
| #pragma once | ||
|
|
||
| #include <cstddef> | ||
| #include <memory> | ||
| #include <vector> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<std::string, std::string> 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,45 +84,24 @@ struct OperatorNode { | |
| struct OperatorNodeHash { | ||
| std::size_t operator()(const OperatorNode &node) const | ||
| { | ||
| // prefer id if available | ||
| if (!node.id.empty()) { | ||
| return std::hash<std::string>{}(node.id); | ||
| } | ||
| return std::hash<std::string>{}(node.name); | ||
| return std::hash<std::string>{}(node.id); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * @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<OperatorNode, double, OperatorNodeHash> 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<double>::infinity(); | ||
| return it != ops.end() ? it->second : std::numeric_limits<double>::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<OperatorNode, RuleNode, OperatorNodeHash | |
| */ | ||
| using AltDecomps = std::unordered_map<OperatorNode, std::vector<RuleNode>, 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have to worry about this in-place mutating base.output?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. |
||
| 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<RuleTerm> inputs; | ||
| double totalCost{0.0}; | ||
| std::unordered_map<OperatorNode, std::size_t, OperatorNodeHash> basisCounts; | ||
|
|
||
| // TODO: revisit this after testing.. | ||
| RuleOrigin origin{RuleOrigin::Default}; | ||
| }; | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we differentiate between adjoint genereated from default, adjoint generated from fixed and adjoint generated from alternative?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No we don't need to consider those cases. Fixed/Alternative are consumed by the builder to enforced which rules exist for an op -- the solver works without knowing about the origin of these rule!
AdjointGeneratedis also consumed by the builder but it's needed in the solver as the solver needs to consider both pathways (in the ADR) and to propagate rules that are built by the solver usingmakeAdjointRule(Pathway 2 in the ADR).