Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions frontend/test/lit/GraphDecomposition/TestGatesetWeights.mlir
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2026 Xanadu Quantum Technologies Inc.
Comment thread
kipawaa marked this conversation as resolved.
Resolved

// 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.

// see https://github.com/llvm/llvm-project/blob/cc6b3b5a1d0fba10d92be143cd52268c62f2a2a9/mlir/test/IR/parser.mlir#L1078 for examples of F64 special values

// Invalid weights must fail with a diagnostic rather than throwing from the solver.
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=TestGate=-1.0})' %s 2>&1 | FileCheck %s --check-prefix=NEGATIVE
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate=-1.0 : f64}})' %s 2>&1 | FileCheck %s --check-prefix=NEGATIVE-F64
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=TestGate=-inf})' %s 2>&1 | FileCheck %s --check-prefix=NEGATIVE-INF
// this is the hex code for MLIR's negative infinity f64
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate = 0xFFF0000000000000 : f64}})' %s 2>&1 | FileCheck %s --check-prefix=NEGATIVE-INF-F64
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=TestGate=nan})' %s 2>&1 | FileCheck %s --check-prefix=NAN
// signalling NaNs
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate = 0x7FF0000000000001 : f64}})' %s 2>&1 | FileCheck %s --check-prefix=NAN-F64
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate = 0x7FF8000000000000 : f64}})' %s 2>&1 | FileCheck %s --check-prefix=NAN-F64
// quiet NaNs
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate = 0x7FF0000001000000 : f64}})' %s 2>&1 | FileCheck %s --check-prefix=NAN-F64
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate = 0xFFF0000001000000 : f64}})' %s 2>&1 | FileCheck %s --check-prefix=NAN-F64
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=TestGate=invalid})' %s 2>&1 | FileCheck %s --check-prefix=MALFORMED
// RUN: not catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=TestGate=})' %s 2>&1 | FileCheck %s --check-prefix=MISSING
// NEGATIVE: error: Invalid cost '-1.0' for gate 'TestGate' in the target gateset: expected a non-negative number (not NaN).
// NEGATIVE-F64: error: Invalid cost '-1.0 : f64' for gate 'TestGate' in the target gateset: expected a non-negative number (not NaN).
// NEGATIVE-INF: error: Invalid cost '-inf' for gate 'TestGate' in the target gateset: expected a non-negative number (not NaN).
// NEGATIVE-INF-F64: error: Invalid cost '0xFFF0000000000000 : f64' for gate 'TestGate' in the target gateset: expected a non-negative number (not NaN).
// NAN: error: Invalid cost 'nan' for gate 'TestGate' in the target gateset: expected a non-negative number (not NaN).
// NAN-F64: error: Invalid cost '{{.+}} : f64' for gate 'TestGate' in the target gateset: expected a non-negative number (not NaN).
// MALFORMED: error: Invalid cost 'invalid' for gate 'TestGate' in the target gateset: expected a non-negative number (not NaN).
// MISSING: error: Invalid cost '' for gate 'TestGate' in the target gateset: expected a non-negative number (not NaN).

// Zero, fractional, and positive infinite weights are permitted, as strings or as float attrs
// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=TestGate=0.0})' %s | FileCheck %s
// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate=0.0 : f64}})' %s | FileCheck %s
// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=TestGate=0.5})' %s | FileCheck %s
// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate=0.5 : f64}})' %s | FileCheck %s
// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set=TestGate=inf})' %s | FileCheck %s
// This is the hex code for MLIR's infinite f64
// RUN: catalyst --tool=opt --pass-pipeline='builtin.module(graph-decomposition{gate-set={TestGate = 0x7FF0000000000000 : f64}})' %s | FileCheck %s

// CHECK-LABEL: func.func @circuit
// CHECK: quantum.custom "TestGate"
func.func @circuit(%q: !quantum.bit) -> !quantum.bit {
%out = quantum.custom "TestGate"() %q : !quantum.bit
return %out : !quantum.bit
}
70 changes: 70 additions & 0 deletions frontend/test/pytest/test_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,22 @@ def test_quiet_by_default(self, capfd):
assert "Decomposition Solution:" not in capture.out + capture.err


@pytest.mark.parametrize("weight", [-1.0, np.nan, -np.inf])
def test_invalid_gateset_weight(weight):
"""Invalid weights supplied through MLIR float attributes fail during gateset parsing."""

@qjit(capture=True, target="mlir")
@graph_decomposition(gate_set={NoParams: weight})
@qnode(qp.device("null.qubit", wires=1))
def circuit():
NoParams(reg=[0])

with pytest.raises(
CompileError, match="Invalid cost .* for gate 'NoParams'.*non-negative number"
):
qp.specs(circuit, level="all")()


def test_gate_already_in_gateset():
"""
Test that decomposing a gate that's already in the gateset works.
Expand All @@ -1788,6 +1804,60 @@ def circuit():
assert "llvm.call @__catalyst__qis__Hadamard" in circuit.mlir_opt


@pytest.mark.parametrize("reverse", [False, True])
def test_cyclic_decompositions_find_cheapest_solution(reverse):
"""A <-> B has exits A -> Target and B -> Target Target.

Both root orders should use the cheaper exit through A.
"""

class A(qp.core.Operator2):
def __init__(self, wires):
super().__init__(wires=wires)

class B(qp.core.Operator2):
def __init__(self, wires):
super().__init__(wires=wires)

class Target(qp.core.Operator2):
def __init__(self, wires):
super().__init__(wires=wires)

@register_resources({B(Wire[1]): 1})
def a_to_b(wires):
B(wires)

@register_resources({A(Wire[1]): 1})
def b_to_a(wires):
A(wires)

@register_resources({Target(Wire[1]): 1})
def a_to_target(wires):
Target(wires)

@register_resources({Target(Wire[1]): 2})
def b_to_targets(wires):
Target(wires)
Target(wires)

with local_decomps():
add_decomps(A, a_to_target, a_to_b)
add_decomps(B, b_to_a, b_to_targets)
gates = (B, A) if reverse else (A, B)

@qjit(capture=True, target="mlir")
@graph_decomposition(gate_set={Target: 1})
@qnode(qp.device("null.qubit", wires=1))
def circuit():
for gate in gates:
gate(wires=0)

resources = qp.specs(circuit, level="all")().resources

assert resources["Before MLIR Passes"].counts == {"A": 1, "B": 1}
assert resources["graph-decomposition"].counts == {"Target": 2}


class TestCustomRuleApplication:
"""Integration tests for applying custom decomposition rules end-to-end."""

Expand Down
4 changes: 2 additions & 2 deletions mlir/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,8 @@ set(TARGETS_TO_REMOVE
PBCUtils
QuantumCAPI
pbc-transforms
quantum-transforms # depends on decompsolver, which depends on boost; exclude
decompsolver # depends on boost; exclude
quantum-transforms # depends on decompsolver; exclude
decompsolver # its public include dir is a source-tree path; exclude
)
list(REMOVE_ITEM CATALYST_INSTALL_TARGETS ${TARGETS_TO_REMOVE})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,75 +1,5 @@
# CMake 3.30 removed the bundled FindBoost module (CMP0167).
# Set to NEW so find_package uses Boost's own BoostConfig.cmake,
# or falls through to FetchContent if not found on the system.
if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()

# This is the version tested and shipped with Ubuntu 24 apt.
# Using older versions (e.g. 1.66 on rhel8 or 1.74 on Ubuntu 22)
# can cause failure, or will require linking the Boost random library
# explicitly. When using 1.83 or newer, the decompsolver target
# will not require external linking to Boost libraries.
set(MIN_BOOST_VERSION 1.83.0)

include(FetchContent)

# The decompsolver target only needs the Boost Graph Library. Linking
# Boost::graph (below) propagates its INTERFACE include dirs automatically,
find_package(Boost ${MIN_BOOST_VERSION} COMPONENTS graph QUIET)

if (Boost_FOUND)
message(STATUS "Found System Boost ${Boost_VERSION}")
else()
message(STATUS "Boost >= ${MIN_BOOST_VERSION} not found on system. Fetching...")

# Restrict Boost's CMake project to the minimal library set needed.
set(BOOST_INCLUDE_LIBRARIES graph CACHE STRING "")

# Ensure BOOST_ENABLE_CMAKE is set BEFORE MakeAvailable
set(BOOST_ENABLE_CMAKE ON CACHE BOOL "")

FetchContent_Declare(
Boost
URL https://github.com/boostorg/boost/releases/download/boost-1.89.0/boost-1.89.0-cmake.tar.xz
URL_HASH SHA256=67acec02d0d118b5de9eb441f5fb707b3a1cdd884be00ca24b9a73c995511f74
DOWNLOAD_EXTRACT_TIMESTAMP ON
SYSTEM # required to prevent deprecation warnings->errors on macOS
)

FetchContent_MakeAvailable(Boost)
Comment thread
kipawaa marked this conversation as resolved.
Resolved

set(BOOST_TARGETS boost_container boost_graph boost_core boost_assert boost_throw_exception boost_config)

foreach(t_name IN LISTS BOOST_TARGETS)
if(TARGET ${t_name})
# Check if the target is an INTERFACE library
get_target_property(target_type ${t_name} TYPE)

if(target_type STREQUAL "INTERFACE_LIBRARY")
# For header-only targets, we set the options as INTERFACE
# so they apply to anyone who includes them (like your code)
target_compile_options(${t_name} INTERFACE -Wno-error -w)
else()
# For compiled targets (like boost_container), we use PRIVATE
# so Boost's own .c/.cpp files compile without -Werror
target_compile_options(${t_name} PRIVATE -Wno-error -w)
endif()
endif()
endforeach()

endif()

# Check that the include_dir doesn't point to a missing dir:
get_target_property(_boost_inc Boost::graph INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "Boost graph headers: ${_boost_inc}")

add_library(decompsolver OBJECT DGBuilder.cpp DGSolver.cpp)

target_link_libraries(decompsolver
PRIVATE Boost::graph
)

target_include_directories(decompsolver PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,13 @@
#include "DGBuilder.hpp"

#include <cstddef>
#include <cstdint>
#include <iostream>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>

#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 "DGTypes.hpp"
#include "DGUtils.hpp"

Expand All @@ -41,56 +34,16 @@ using namespace DecompGraph::Core;
namespace DecompGraph::Solver {

struct DecompositionGraph::Impl {
using RuleId = DecompositionGraph::RuleId;
using OperatorId = std::size_t;

struct OperatorVertex {
OperatorId op_id;
};

struct RuleVertex {
RuleId rule_id;
};

enum class VertexType : std::uint8_t { Operator = 0, Rule = 1 };

struct GraphVertex {
VertexType type;
std::variant<OperatorVertex, RuleVertex> payload;
};

struct GraphWeightedEdge {}; // placeholder used in the boost graph as a type

using BbGraph = boost::adjacency_list<boost::vecS, boost::vecS, boost::bidirectionalS,
GraphVertex, GraphWeightedEdge>;
using Vertex = boost::graph_traits<BbGraph>::vertex_descriptor;

BbGraph graph;
std::vector<OperatorNode> operators;
WeightedGateset gateset;
std::vector<RuleNode> rules;

std::unordered_map<OperatorNode, OperatorId, OperatorNodeHash> opToId;
std::vector<OperatorNode> idToOp;
std::unordered_map<OperatorId, Vertex> opIdToVertex;

std::unordered_map<RuleId, Vertex> ruleIdToVertex;
std::unordered_map<OperatorNode, std::vector<RuleNode>, OperatorNodeHash> opToRules;

OperatorId registerOp(const OperatorNode &op) {
const auto it = opToId.find(op);
if (it != opToId.end()) {
return it->second;
}

OperatorId newId = opToId.size();
opToId.emplace(op, newId);
idToOp.push_back(op);
const auto vertex =
boost::add_vertex(GraphVertex{VertexType::Operator, OperatorVertex{newId}}, graph);
opIdToVertex.emplace(newId, vertex);
return newId;
}
void registerOp(const OperatorNode &op) { opToId.emplace(op, opToId.size()); }

Impl(std::vector<OperatorNode> _operators, WeightedGateset _gateset,
std::vector<RuleNode> _rules)
Expand Down Expand Up @@ -130,37 +83,15 @@ struct DecompositionGraph::Impl {
}

void buildGraph() {
// Register all operators
for (const auto &op : operators) {
registerOp(op);
}

// Register all rules
for (RuleId ruleId = 0; ruleId < rules.size(); ruleId++) {
const auto &rule = rules[ruleId];
const auto id = registerOp(rule.output);
const auto output_vertex = opIdToVertex[id];

// Create a vertex for the rule and connect it to its output operator vertex
const auto rule_vertex =
boost::add_vertex(GraphVertex{VertexType::Rule, RuleVertex{ruleId}}, graph);
ruleIdToVertex.emplace(ruleId, rule_vertex);
for (const auto &rule : rules) {
registerOp(rule.output);
opToRules[rule.output].push_back(rule);

// Connect rule vertex to output operator vertex
boost::add_edge(rule_vertex, output_vertex, GraphWeightedEdge{}, graph);

// Empty rules (with no inputs) are effectively just target gates
// and don't need to be connected to input operator vertices
if (rule.isEmpty()) {
continue;
}

// Connect rule vertex to input operator vertices
for (const auto &input : rule.inputs) {
const auto input_id = registerOp(input.op);
const auto input_vertex = opIdToVertex[input_id];
boost::add_edge(input_vertex, rule_vertex, GraphWeightedEdge{}, graph);
registerOp(input.op);
}
}
}
Expand Down Expand Up @@ -238,7 +169,7 @@ void showGraph(const Solver::DecompositionGraph &graph, std::ostream &os) {

// Show all rules by their names and their input/output operators
os << "Rules:\n";
for (const auto &[ruleId, _] : impl->ruleIdToVertex) {
for (std::size_t ruleId = 0; ruleId < impl->rules.size(); ++ruleId) {
const auto &rule = impl->rules[ruleId];
os << " Rule ID " << ruleId << ": " << rule.name;
os << "\n";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
/**
* @file DGBuilder.hpp
*
* @brief This file implements the DecompositionGraph class, which constructs a graph
* representation of the decomposition problem based on the provided operators, gateset,
* and rules. The graph is built using the Boost Graph Library, where vertices represent
* operators and rules, and edges represent the relationships between them according to
* the decomposition rules.
* @brief This file implements the DecompositionGraph class, which holds the decomposition problem
* defined by the provided operators, gateset, and rules. Operators are the graph's nodes and each
* rule is a hyperedge from its input operators to its output operator; the graph is stored as a
* map from each operator to the rules that decompose it, which is all the solver needs to traverse
* it.
*/

#pragma once
Expand Down Expand Up @@ -49,10 +49,9 @@ class DecompositionGraph {
/**
* @brief Constructs the decomposition graph from the given operators, gateset, and rules.
*
* The constructor initializes the graph by registering all operators and rules, creating
* vertices for them, and adding edges according to the decomposition rules. The graph is
* built in such a way that it can be efficiently traversed by the decomposition solver
* to find optimal decompositions of the target gates.
* The constructor registers every operator (the given ones and those appearing in rules) and
* indexes the rules by the operator they decompose, so the solver can look up an operator's
* rules directly.
*
* @param operators The list of operators that are used in the decomposition rules and gateset.
* @param gateset The target gateset that we want to decompose into, along with their
Expand Down
Loading
Loading