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
Binary file added edge-sketch.pdf
Binary file not shown.
40 changes: 40 additions & 0 deletions include/sgl/constants.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include "gl/constants.hpp"
#include "sgl/traits.hpp"

#include <cstddef>
#include <limits>

namespace sgl {

using gl::initial_id;
using gl::initial_id_t;
using gl::initial_id_v;
using gl::invalid_id;
using gl::invalid_id_t;
using gl::invalid_id_v;

inline constexpr std::size_t default_sketch_width = 64uz;

inline constexpr std::size_t cache_line_size = 64uz;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


using traits::min_hash_word_size;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be defined in both sgl:: and sgl::traits at the same time - choose one


template <traits::c_weight_type WeightType>
inline constexpr WeightType sketch_infinity_v = std::numeric_limits<WeightType>::infinity();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also introduce a scetch_infinity_t type and a scetch_infinity constant (same as for invalid_id_t, invalid_id)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also define a scetch_inifity_t type and scetch_infinity constant as for ininitial/invalid_id constants


[[nodiscard]] constexpr std::size_t align_sketch_stride(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be defined in constants.hpp

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be defined in constants.hpp

const std::size_t m, const std::size_t slot_size
) noexcept {
if (m == 0uz or slot_size == 0uz)
return m;

const auto slots_per_line = cache_line_size / slot_size;
if (slots_per_line == 0uz)
return m;

return ((m + slots_per_line - 1uz) / slots_per_line) * slots_per_line;
}

} // namespace sgl
21 changes: 21 additions & 0 deletions include/sgl/directional_tags.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include "gl/directional_tags.hpp"
#include "sgl/traits.hpp"

namespace sgl {

using directed_t = gl::directed_t;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, we could be just doing use gl::directed_t


using undirected_t = gl::undirected_t;

namespace traits {

template <typename T>
concept c_sketch_directional_tag = c_one_of<T, directed_t, undirected_t>;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such requirement is already defined as c_directional_tag in gl - maybe it could be reused?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the same as c_graph_directional_tag?
If yes, then either:

  • use only the scetch concept to be explicit (and protect the module from possible additions of directional tags in the gl module) (preferable)
  • use only the graph concept if you don't need to be as explicit here


using gl::traits::c_graph_directional_tag;

} // namespace traits

} // namespace sgl
135 changes: 135 additions & 0 deletions include/sgl/edge_key.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#pragma once

#include "sgl/traits.hpp"
#include "sgl/types.hpp"

#include <algorithm>
#include <concepts>
#include <ranges>
#include <span>
#include <vector>

namespace sgl {

template <traits::c_id_type IdType = default_id_type>
struct binary_edge_key {
using id_type = IdType;
using key_type = homogeneous_pair<id_type>;

[[nodiscard]] static constexpr key_type make(const id_type u, const id_type v) noexcept {
return (u <= v) ? key_type{u, v} : key_type{v, u};

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return std::minmax(u, v)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return std::minmax(u, v)

}

[[nodiscard]] static constexpr key_type make_directed(const id_type u, const id_type v) noexcept {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this actually work like this?
From what I undestand the directed key should be strictly ordered as (u, v)
And if you call make(u, v) it might swap these values if v > u

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be like this?
From what I undestand for directed edges the key should be an ordered (u, v) pair and now you are calling make(u, v) which will switch these values if v > u

return make(u, v);
}
};

template <
traits::c_id_type IdType = default_id_type,
std::unsigned_integral OccurrenceType = IdType>
struct multi_edge_key_value {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be defined as a type within multi_edge_key
So just:

struct multi_edge_key {
  struct key_type {
    // ...
  };
};

or:

struct multi_edge_key {
  struct key_value {
    // ...
  };
  using key_type = key_value;
};

or something similar

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be defined as a inner type within the multi_edge_key type:

struct multi_edge_key {
  struct key_type {
    // ...
  };
};

or:

struct multi_edge_key {
  struct key_value {
    // ...
  };
  using key_type = key_value;
};

using id_type = IdType;
using occurrence_type = OccurrenceType;

id_type first{};
id_type second{};
occurrence_type occurrence{};

[[nodiscard]] constexpr bool operator==(const multi_edge_key_value&) const noexcept = default;
};

template <
traits::c_id_type IdType = default_id_type,
std::unsigned_integral OccurrenceType = IdType>
struct multi_edge_key {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multi_edge or multiedge?

using id_type = IdType;
using occurrence_type = OccurrenceType;
using key_type = multi_edge_key_value<IdType, OccurrenceType>;

[[nodiscard]] static constexpr key_type make(
const id_type u, const id_type v, const occurrence_type occurrence
) noexcept {
const auto [a, b] = binary_edge_key<id_type>::make(u, v);
return key_type{a, b, occurrence};
}
};

template <traits::c_id_type IdType = default_id_type, typename LabelType = IdType>
struct labeled_edge_key_value {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for multi_edge_key_value

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as for multiedge

using id_type = IdType;
using label_type = LabelType;

id_type first{};
id_type second{};
label_type label{};

[[nodiscard]] constexpr bool operator==(const labeled_edge_key_value&) const noexcept = default;
};

template <traits::c_id_type IdType = default_id_type, typename LabelType = IdType>
struct labeled_edge_key {
using id_type = IdType;
using label_type = LabelType;
using key_type = labeled_edge_key_value<IdType, LabelType>;

[[nodiscard]] static constexpr key_type make(
const id_type u, const id_type v, const label_type& label
) noexcept {
const auto [a, b] = binary_edge_key<id_type>::make(u, v);
return key_type{a, b, label};
}
};

template <traits::c_id_type IdType = default_id_type>
struct hyper_edge_key {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hyperedge

using id_type = IdType;
using key_type = std::vector<id_type>;

[[nodiscard]] static key_type make(std::span<const id_type> endpoints) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe std::span is a sized range, so this is reduntant

key_type key(endpoints.begin(), endpoints.end());
std::ranges::sort(key);
key.erase(std::ranges::unique(key).begin(), key.end());
return key;
}

template <traits::c_sized_range_of<id_type> R>
[[nodiscard]] static key_type make(const R& endpoints) {
key_type key;
key.reserve(std::ranges::size(endpoints));
for (const auto id : endpoints)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All ranges must have a begin() and end() methods so you could just do the same thing as in the span overload

Moreover, then you don't need to reserve the space because std::vector will do that for you if the type of the range allows for this. Because of that this function could theoretically take as input any range (including input ranges)

Additionally you should define the function paramter as R&& instead of const R& and you could get rid of the explicit template parameter and do make (traits::c_range_of<id_type> auto&& endpoints)

One more thing - stating that a hyperedge has endpoints is a bit incorrect semantically (for undirected hyperedges)

key.push_back(id);
std::ranges::sort(key);
key.erase(std::ranges::unique(key).begin(), key.end());
return key;
}
};

namespace traits {

template <typename KeyPolicy>
concept c_edge_key_policy = requires {
typename KeyPolicy::id_type;
typename KeyPolicy::key_type;
requires c_id_type<typename KeyPolicy::id_type>;
};

template <typename KeyPolicy>
concept c_binary_edge_key_policy =
c_edge_key_policy<KeyPolicy> and c_instantiation_of<KeyPolicy, binary_edge_key>;

template <typename KeyPolicy>
concept c_multi_edge_key_policy =
c_edge_key_policy<KeyPolicy> and c_instantiation_of<KeyPolicy, multi_edge_key>;

template <typename KeyPolicy>
concept c_labeled_edge_key_policy =
c_edge_key_policy<KeyPolicy> and c_instantiation_of<KeyPolicy, labeled_edge_key>;

template <typename KeyPolicy>
concept c_hyper_edge_key_policy =
c_edge_key_policy<KeyPolicy> and c_instantiation_of<KeyPolicy, hyper_edge_key>;

} // namespace traits

} // namespace sgl
166 changes: 166 additions & 0 deletions include/sgl/hash.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#pragma once

#include "sgl/edge_key.hpp"
#include "sgl/traits.hpp"
#include "sgl/types.hpp"

#include <limits>
#include <type_traits>
#include <vector>

namespace sgl {

namespace detail {

template <traits::c_hash_word_type HashWord>
[[nodiscard]] constexpr HashWord splitmix(HashWord x) noexcept {
x += static_cast<HashWord>(0x9e3779b97f4a7c15ULL);
x = (x ^ (x >> 30)) * static_cast<HashWord>(0xbf58476d1ce4e5b9ULL);
x = (x ^ (x >> 27)) * static_cast<HashWord>(0x94d049bb133111ebULL);
return x ^ (x >> 31);
}

template <traits::c_hash_word_type HashWord>
[[nodiscard]] constexpr HashWord mix(HashWord a, HashWord b) noexcept {
return splitmix<HashWord>(
a + static_cast<HashWord>(0x9e3779b97f4a7c15ULL) + (b << 6) + (b >> 2)
);
}

template <traits::c_hash_word_type HashWord, traits::c_uniform_type UniformType>
[[nodiscard]] constexpr UniformType hash_to_unit_interval(HashWord h) noexcept {
constexpr auto mantissa_bits = static_cast<HashWord>(
std::numeric_limits<UniformType>::digits > 0 ? std::numeric_limits<UniformType>::digits : 53
);
constexpr auto shift = static_cast<HashWord>(sizeof(HashWord) * 8) > mantissa_bits
? static_cast<HashWord>(sizeof(HashWord) * 8) - mantissa_bits
: HashWord{0};
constexpr auto scale =
UniformType{1} / static_cast<UniformType>(HashWord{1} << mantissa_bits);
return static_cast<UniformType>((h >> shift) + HashWord{1}) * scale;
}

template <traits::c_hash_word_type HashWord, traits::c_id_type IdType>
[[nodiscard]] constexpr HashWord hash_id(IdType id) noexcept {
return splitmix<HashWord>(static_cast<HashWord>(id));
}

template <traits::c_hash_word_type HashWord, typename T>
[[nodiscard]] constexpr HashWord hash_value(const T& value) noexcept {
if constexpr (std::unsigned_integral<T>) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can collapse the signed/unsigned cases to just one branch: if constexpr (std::integral<T>)

return splitmix<HashWord>(static_cast<HashWord>(value));
}
else if constexpr (std::signed_integral<T>) {
return splitmix<HashWord>(static_cast<HashWord>(value));
}
else {
HashWord h{};
const auto* bytes = reinterpret_cast<const unsigned char*>(&value);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reinterpret_cast is unsafe -> use std::bit_cast

for (size_type i = 0uz; i < sizeof(T); ++i)
h = mix<HashWord>(h, static_cast<HashWord>(bytes[i]));
return h;
}
}

} // namespace detail

template <
traits::c_hash_word_type HashWord = default_hash_word_type,
traits::c_uniform_type UniformType = default_uniform_type>
struct basic_edge_hash {
using hash_word_type = HashWord;
using seed_type = HashWord;
using uniform_type = UniformType;

template <traits::c_id_type IdType>
[[nodiscard]] static constexpr seed_type seed(const homogeneous_pair<IdType>& key) noexcept {
return detail::mix<hash_word_type>(
detail::hash_id<hash_word_type>(key.first),
detail::hash_id<hash_word_type>(key.second)
);
}

template <traits::c_id_type IdType>
[[nodiscard]] static constexpr uniform_type uniform(
const homogeneous_pair<IdType>& key, const size_type k
) noexcept {
return detail::hash_to_unit_interval<hash_word_type, uniform_type>(
detail::mix<hash_word_type>(seed(key), static_cast<hash_word_type>(k))
);
}

template <traits::c_id_type IdType, std::unsigned_integral Occ>
[[nodiscard]] static constexpr seed_type seed(
const multi_edge_key_value<IdType, Occ>& key
) noexcept {
return detail::mix<hash_word_type>(
seed(homogeneous_pair<IdType>{key.first, key.second}),
detail::hash_value<hash_word_type>(key.occurrence)
);
}

template <traits::c_id_type IdType, std::unsigned_integral Occ>
[[nodiscard]] static constexpr uniform_type uniform(
const multi_edge_key_value<IdType, Occ>& key, const size_type k
) noexcept {
return detail::hash_to_unit_interval<hash_word_type, uniform_type>(
detail::mix<hash_word_type>(seed(key), static_cast<hash_word_type>(k))
);
}

template <traits::c_id_type IdType, typename LabelType>
[[nodiscard]] static seed_type seed(
const labeled_edge_key_value<IdType, LabelType>& key
) noexcept {
return detail::mix<hash_word_type>(
seed(homogeneous_pair<IdType>{key.first, key.second}),
detail::hash_value<hash_word_type>(key.label)
);
}

template <traits::c_id_type IdType, typename LabelType>
[[nodiscard]] static uniform_type uniform(
const labeled_edge_key_value<IdType, LabelType>& key, const size_type k
) noexcept {
return detail::hash_to_unit_interval<hash_word_type, uniform_type>(
detail::mix<hash_word_type>(seed(key), static_cast<hash_word_type>(k))
);
}

template <traits::c_id_type IdType>
[[nodiscard]] static seed_type seed(const std::vector<IdType>& key) noexcept {
seed_type h = static_cast<seed_type>(0x9e3779b97f4a7c15ULL);
for (const auto id : key)
h = detail::mix<hash_word_type>(h, detail::hash_id<hash_word_type>(id));
return h;
}

template <traits::c_id_type IdType>
[[nodiscard]] static uniform_type uniform(
const std::vector<IdType>& key, const size_type k
) noexcept {
return detail::hash_to_unit_interval<hash_word_type, uniform_type>(
detail::mix<hash_word_type>(seed(key), static_cast<hash_word_type>(k))
);
}
};

using default_edge_hash = basic_edge_hash<>;

namespace traits {

template <typename HashPolicy>
concept c_edge_hash_policy = requires(
const homogeneous_pair<default_id_type>& key, size_type k

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably should not be using default_id_type but accept an IdType as a template parameter and validate against it:

template <typename HashPolicy, typename IdType>
concept c_edge_hash_policy = requires(const homogeneous_pair<IdType>& key), ...) { ... };

) {
typename HashPolicy::seed_type;
typename HashPolicy::uniform_type;
requires c_hash_word_type<typename HashPolicy::seed_type>;
requires c_uniform_type<typename HashPolicy::uniform_type>;
{ HashPolicy::seed(key) } -> std::convertible_to<typename HashPolicy::seed_type>;
{ HashPolicy::uniform(key, k) } -> std::convertible_to<typename HashPolicy::uniform_type>;
};

} // namespace traits

} // namespace sgl
Loading
Loading