Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
working-directory: ./build
run: ./louds_tree_tests

- name: Run DFUDS Tree Tests
working-directory: ./build
run: ./dfuds_tree_tests

- name: Run Benchmark Tests
working-directory: ./build
run: ./benchmark_tests
Expand Down Expand Up @@ -92,3 +96,15 @@ jobs:
fi
exit $rc

- name: Run DFUDS Tree Tests
working-directory: ./build
run: |
timeout 1800 sde-external-9.58.0-2025-06-16-lin/sde64 -icl -emu-xinuse 0 -- \
./dfuds_tree_tests --gtest_output=xml:dfuds_results.xml
rc=$?
if [ $rc -eq 124 ] && grep -q 'failures="0"' dfuds_results.xml 2>/dev/null; then
echo "SDE timed out during process teardown (known SDE/ASan issue) - all tests passed, treating as success"
exit 0
fi
exit $rc

18 changes: 18 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ if(PIXIE_TESTS)
gtest
gtest_main
${PIXIE_DIAGNOSTICS_LIBS})

add_executable(dfuds_tree_tests
src/tests/dfuds_tree_tests.cpp)
target_include_directories(dfuds_tree_tests
PUBLIC include)
target_link_libraries(dfuds_tree_tests
gtest
gtest_main
${PIXIE_DIAGNOSTICS_LIBS})
endif()

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -201,6 +210,15 @@ if(PIXIE_BENCHMARKS)
benchmark_main
${PIXIE_DIAGNOSTICS_LIBS})

add_executable(dfuds_tree_benchmarks
src/benchmarks/dfuds_tree_benchmarks.cpp)
target_include_directories(dfuds_tree_benchmarks
PUBLIC include)
target_link_libraries(dfuds_tree_benchmarks
benchmark
benchmark_main
${PIXIE_DIAGNOSTICS_LIBS})

add_executable(alignment_comparison
src/benchmarks/alignment_comparison.cpp)
target_include_directories(alignment_comparison
Expand Down
162 changes: 162 additions & 0 deletions include/pixie/dfuds_tree.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#pragma once

#include <pixie/rmm_tree.h>

#include <cstdint>

#include "utils.h"

namespace pixie {

/**
* @brief A tree class based on the depth-first unary degree sequence (DFUDS)
* representation
*/
class DFUDSTree {
private:
const size_t num_bits_;
RmMTree rmm_;

public:
struct Node {
size_t number;

size_t pos;

/**
* @brief A node class of DFUDS tree
*/
Node(size_t node_number, size_t dfuds_pos)
: number(node_number), pos(dfuds_pos) {}
};

/**
* @brief Constructor from an external array of uint64_t
*
* @param dfuds_sequence parenthesis sequence in dfuds representation
*/
explicit DFUDSTree(const std::vector<std::uint64_t>& dfuds_sequence,
size_t tree_size)
: num_bits_(2 * tree_size - 1), rmm_(dfuds_sequence, 2 * tree_size - 1) {}

/**
* @brief Returns the root node
*/
static Node root() { return Node(0, 0); }

/**
* @brief Returns the size of the tree
*/
size_t size() const { return (num_bits_ + 1) / 2; }

/**
* @brief Indicates if @p node is a leaf
*/
bool is_leaf(const Node& node) const {
return (node.pos + 1 == num_bits_) or rmm_.bit(node.pos) == 0;
}

/**
* @brief Indicates if @p node is a root
*/
bool is_root(const Node& node) const { return node.number == 0; }

/**
* @brief Returns the number of children of a @p node
*/
size_t degree(const Node& node) const {
return rmm_.select0(node.number + 1) - node.pos;
}

/**
* @brief Returns first child of a @p node
*/
Node first_child(const Node& node) {
size_t pos = rmm_.select0(node.number + 1);
size_t num = node.number + 1;
return Node(num, pos + 1);
}

/**
* @brief Returns the i-th child of @p node
* Indexing starts at 0
*/
Node child(const Node& node, size_t i) const {
size_t pos = rmm_.close(rmm_.select0(node.number + 1) - i) + 1;
size_t num = rmm_.rank0(pos);
return Node(num, pos);
}

/**
* @brief Returns next sibling of a @p node
*/
Node next_sibling(const Node& node) const {
size_t end = rmm_.fwdsearch(node.pos, -1);
size_t pos = end + 1;
size_t num = rmm_.rank0(pos);
return Node(num, pos);
}

/**
* @brief Returns the parent of a @p node if @p node is not root,
* else returns root
*/
Node parent(const Node& node) const {
if (node.number == 0) {
return root();
}
size_t open = rmm_.open(
node.pos); // node.pos in 0-based and rmm_.open uses 1-based argument.
// Thus, we use node.pos meaning the parenthesis before
// first parenthesis of the current node
size_t rank = rmm_.rank0(open);
size_t pos =
rmm_.select0(rank) +
1; // In here we use that rmm_select(0) equals size_t max value so
// rmm_.select(rank) can still be interpreted as pos-1
return Node(rank, pos);
}

/**
* @brief Indicates if @p node is last child
*/
bool is_last_child(const Node& node) const {
size_t end = rmm_.fwdsearch(node.pos, -1);
size_t pos = end + 1;
size_t op = rmm_.open(node.pos);
size_t op2 = rmm_.open(pos);
return pos == num_bits_ || op != op2 + 1;
}
};

std::vector<uint64_t> adj_to_dfuds(
size_t tree_size,
const std::vector<std::vector<size_t>>& adj) {
size_t dfuds_size = tree_size * 2 - 1;
std::vector<uint64_t> dfuds((dfuds_size + 63) / 64, 0);
std::vector<size_t> stack;
stack.push_back(0);
size_t pos = 0;
while (!stack.empty()) {
auto v = stack.back();
stack.pop_back();
size_t edge_count = adj[v].size();
for (size_t i = 0; i < edge_count - 1; ++i) { // edge 0 goes to parent
dfuds[pos >> 6] = dfuds[pos >> 6] | (1ULL << (pos & 63));
pos++;
stack.push_back(adj[v][edge_count - 1 - i]);
}
pos++;
}
return dfuds;
}

bool operator==(const AdjListNode& a, const DFUDSTree::Node& b) {
return a.number == b.number;
}

bool operator==(const DFUDSTree::Node& b, const AdjListNode& a) {
return a.number == b.number;
}

} // namespace pixie
14 changes: 7 additions & 7 deletions include/pixie/rmm_tree.h
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,13 @@ class RmMTree {
return (result == npos ? npos : result + 1);
}

/**
* @brief Read bit at position @p position (LSB-first across words).
*/
inline int bit(const size_t& position) const noexcept {
return (bits[position >> 6] >> (position & 63)) & 1u;
}

private:
/**
* @brief Count "10" occurrences inside a 64-bit slice of given logical
Expand Down Expand Up @@ -2215,13 +2222,6 @@ class RmMTree {
build(leaf_block_bits, max_overhead);
}

/**
* @brief Read bit at position @p position (LSB-first across words).
*/
inline int bit(const size_t& position) const noexcept {
return (bits[position >> 6] >> (position & 63)) & 1u;
}

/**
* @brief Set bit at position @p position to 1.
*/
Expand Down
26 changes: 26 additions & 0 deletions include/pixie/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,32 @@ std::vector<std::vector<size_t>> bfs_order(
return bfs_adj;
}

std::vector<std::vector<size_t>> dfs_order(
size_t tree_size,
const std::vector<std::vector<size_t>>& adj) {
std::vector<std::vector<size_t>> dfs_adj(tree_size);
std::vector<std::pair<size_t, size_t>> stack;
dfs_adj[0].push_back(0);
stack.push_back({0, 0});
std::vector<size_t> renumbering(tree_size, 0);
size_t next_number = 1;
while (!stack.empty()) {
auto& [v, i] = stack.back();
i++;
if (i == adj[v].size()) {
stack.pop_back();
continue;
}
size_t u = adj[v][i];
renumbering[u] = next_number++;
dfs_adj[renumbering[v]].push_back(renumbering[u]);
dfs_adj[renumbering[u]].push_back(renumbering[v]);

stack.push_back(std::pair{u, 0});
}
return dfs_adj;
}

std::vector<uint64_t> adj_to_louds(
size_t tree_size,
const std::vector<std::vector<size_t>>& adj) {
Expand Down
1 change: 1 addition & 0 deletions scripts/coverage_report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ cmake --build --preset coverage
"${BUILD_DIR}/unittests"
"${BUILD_DIR}/excess_positions_tests"
"${BUILD_DIR}/louds_tree_tests"
"${BUILD_DIR}/dfuds_tree_tests"
"${BUILD_DIR}/test_rmm"

cd "${BUILD_DIR}"
Expand Down
68 changes: 68 additions & 0 deletions src/benchmarks/dfuds_tree_benchmarks.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include <benchmark/benchmark.h>
#include <pixie/dfuds_tree.h>
#include <pixie/utils.h>

#include <random>

using Node = pixie::DFUDSTree::Node;
using pixie::adj_to_dfuds;
using pixie::DFUDSTree;

/**
* DFS with O(1) extra memory
*/
static void BM_DfudsTreeDFS(benchmark::State& state) {
size_t tree_size = state.range(0);
std::mt19937_64 rng(42);

for (auto _ : state) {
state.PauseTiming();

std::vector<std::vector<size_t>> adj = generate_random_tree(tree_size, rng);
adj = dfs_order(tree_size, adj);
std::vector<uint64_t> dfuds = adj_to_dfuds(tree_size, adj);
DFUDSTree tree(dfuds, tree_size);

Node cur = tree.root();
bool above = 1;

state.ResumeTiming();

benchmark::DoNotOptimize(cur);

while (true) {
if (above) {
if (tree.is_leaf(cur)) {
above = 0;
} else {
cur = tree.first_child(cur);
}
benchmark::DoNotOptimize(cur);
} else {
if (tree.is_last_child(cur)) {
cur = tree.parent(cur);
if (tree.is_root(cur)) {
break;
}
benchmark::DoNotOptimize(cur);
} else {
cur = tree.next_sibling(cur);
above = 1;
benchmark::DoNotOptimize(cur);
}
}
}
}
}

BENCHMARK(BM_DfudsTreeDFS)
->ArgNames({"tree_size"})
->RangeMultiplier(2)
->Range(1ull << 8, 1ull << 18)
->Iterations(100);

BENCHMARK(BM_DfudsTreeDFS)
->ArgNames({"tree_size"})
->RangeMultiplier(2)
->Range(1ull << 18, 1ull << 26)
->Iterations(10);
Loading
Loading