diff --git a/edge-sketch.pdf b/edge-sketch.pdf new file mode 100644 index 00000000..5ced087c Binary files /dev/null and b/edge-sketch.pdf differ diff --git a/include/sgl/constants.hpp b/include/sgl/constants.hpp new file mode 100644 index 00000000..0312afd0 --- /dev/null +++ b/include/sgl/constants.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "gl/constants.hpp" +#include "sgl/traits.hpp" + +#include +#include + +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; + +using traits::min_hash_word_size; + +template +inline constexpr WeightType sketch_infinity_v = std::numeric_limits::infinity(); + +[[nodiscard]] constexpr std::size_t align_sketch_stride( + 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 diff --git a/include/sgl/directional_tags.hpp b/include/sgl/directional_tags.hpp new file mode 100644 index 00000000..b48e1eca --- /dev/null +++ b/include/sgl/directional_tags.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "gl/directional_tags.hpp" +#include "sgl/traits.hpp" + +namespace sgl { + +using directed_t = gl::directed_t; + +using undirected_t = gl::undirected_t; + +namespace traits { + +template +concept c_sketch_directional_tag = c_one_of; + +using gl::traits::c_graph_directional_tag; + +} // namespace traits + +} // namespace sgl diff --git a/include/sgl/edge_key.hpp b/include/sgl/edge_key.hpp new file mode 100644 index 00000000..0ad7abd5 --- /dev/null +++ b/include/sgl/edge_key.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include "sgl/traits.hpp" +#include "sgl/types.hpp" + +#include +#include +#include +#include +#include + +namespace sgl { + +template +struct binary_edge_key { + using id_type = IdType; + using key_type = homogeneous_pair; + + [[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}; + } + + [[nodiscard]] static constexpr key_type make_directed(const id_type u, const id_type v) noexcept { + return make(u, v); + } +}; + +template < + traits::c_id_type IdType = default_id_type, + std::unsigned_integral OccurrenceType = IdType> +struct multi_edge_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 { + using id_type = IdType; + using occurrence_type = OccurrenceType; + using key_type = multi_edge_key_value; + + [[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::make(u, v); + return key_type{a, b, occurrence}; + } +}; + +template +struct labeled_edge_key_value { + 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 +struct labeled_edge_key { + using id_type = IdType; + using label_type = LabelType; + using key_type = labeled_edge_key_value; + + [[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::make(u, v); + return key_type{a, b, label}; + } +}; + +template +struct hyper_edge_key { + using id_type = IdType; + using key_type = std::vector; + + [[nodiscard]] static key_type make(std::span endpoints) { + key_type key(endpoints.begin(), endpoints.end()); + std::ranges::sort(key); + key.erase(std::ranges::unique(key).begin(), key.end()); + return key; + } + + template R> + [[nodiscard]] static key_type make(const R& endpoints) { + key_type key; + key.reserve(std::ranges::size(endpoints)); + for (const auto id : endpoints) + key.push_back(id); + std::ranges::sort(key); + key.erase(std::ranges::unique(key).begin(), key.end()); + return key; + } +}; + +namespace traits { + +template +concept c_edge_key_policy = requires { + typename KeyPolicy::id_type; + typename KeyPolicy::key_type; + requires c_id_type; +}; + +template +concept c_binary_edge_key_policy = + c_edge_key_policy and c_instantiation_of; + +template +concept c_multi_edge_key_policy = + c_edge_key_policy and c_instantiation_of; + +template +concept c_labeled_edge_key_policy = + c_edge_key_policy and c_instantiation_of; + +template +concept c_hyper_edge_key_policy = + c_edge_key_policy and c_instantiation_of; + +} // namespace traits + +} // namespace sgl diff --git a/include/sgl/hash.hpp b/include/sgl/hash.hpp new file mode 100644 index 00000000..3817ed59 --- /dev/null +++ b/include/sgl/hash.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include "sgl/edge_key.hpp" +#include "sgl/traits.hpp" +#include "sgl/types.hpp" + +#include +#include +#include + +namespace sgl { + +namespace detail { + +template +[[nodiscard]] constexpr HashWord splitmix(HashWord x) noexcept { + x += static_cast(0x9e3779b97f4a7c15ULL); + x = (x ^ (x >> 30)) * static_cast(0xbf58476d1ce4e5b9ULL); + x = (x ^ (x >> 27)) * static_cast(0x94d049bb133111ebULL); + return x ^ (x >> 31); +} + +template +[[nodiscard]] constexpr HashWord mix(HashWord a, HashWord b) noexcept { + return splitmix( + a + static_cast(0x9e3779b97f4a7c15ULL) + (b << 6) + (b >> 2) + ); +} + +template +[[nodiscard]] constexpr UniformType hash_to_unit_interval(HashWord h) noexcept { + constexpr auto mantissa_bits = static_cast( + std::numeric_limits::digits > 0 ? std::numeric_limits::digits : 53 + ); + constexpr auto shift = static_cast(sizeof(HashWord) * 8) > mantissa_bits + ? static_cast(sizeof(HashWord) * 8) - mantissa_bits + : HashWord{0}; + constexpr auto scale = + UniformType{1} / static_cast(HashWord{1} << mantissa_bits); + return static_cast((h >> shift) + HashWord{1}) * scale; +} + +template +[[nodiscard]] constexpr HashWord hash_id(IdType id) noexcept { + return splitmix(static_cast(id)); +} + +template +[[nodiscard]] constexpr HashWord hash_value(const T& value) noexcept { + if constexpr (std::unsigned_integral) { + return splitmix(static_cast(value)); + } + else if constexpr (std::signed_integral) { + return splitmix(static_cast(value)); + } + else { + HashWord h{}; + const auto* bytes = reinterpret_cast(&value); + for (size_type i = 0uz; i < sizeof(T); ++i) + h = mix(h, static_cast(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 + [[nodiscard]] static constexpr seed_type seed(const homogeneous_pair& key) noexcept { + return detail::mix( + detail::hash_id(key.first), + detail::hash_id(key.second) + ); + } + + template + [[nodiscard]] static constexpr uniform_type uniform( + const homogeneous_pair& key, const size_type k + ) noexcept { + return detail::hash_to_unit_interval( + detail::mix(seed(key), static_cast(k)) + ); + } + + template + [[nodiscard]] static constexpr seed_type seed( + const multi_edge_key_value& key + ) noexcept { + return detail::mix( + seed(homogeneous_pair{key.first, key.second}), + detail::hash_value(key.occurrence) + ); + } + + template + [[nodiscard]] static constexpr uniform_type uniform( + const multi_edge_key_value& key, const size_type k + ) noexcept { + return detail::hash_to_unit_interval( + detail::mix(seed(key), static_cast(k)) + ); + } + + template + [[nodiscard]] static seed_type seed( + const labeled_edge_key_value& key + ) noexcept { + return detail::mix( + seed(homogeneous_pair{key.first, key.second}), + detail::hash_value(key.label) + ); + } + + template + [[nodiscard]] static uniform_type uniform( + const labeled_edge_key_value& key, const size_type k + ) noexcept { + return detail::hash_to_unit_interval( + detail::mix(seed(key), static_cast(k)) + ); + } + + template + [[nodiscard]] static seed_type seed(const std::vector& key) noexcept { + seed_type h = static_cast(0x9e3779b97f4a7c15ULL); + for (const auto id : key) + h = detail::mix(h, detail::hash_id(id)); + return h; + } + + template + [[nodiscard]] static uniform_type uniform( + const std::vector& key, const size_type k + ) noexcept { + return detail::hash_to_unit_interval( + detail::mix(seed(key), static_cast(k)) + ); + } +}; + +using default_edge_hash = basic_edge_hash<>; + +namespace traits { + +template +concept c_edge_hash_policy = requires( + const homogeneous_pair& key, size_type k +) { + typename HashPolicy::seed_type; + typename HashPolicy::uniform_type; + requires c_hash_word_type; + requires c_uniform_type; + { HashPolicy::seed(key) } -> std::convertible_to; + { HashPolicy::uniform(key, k) } -> std::convertible_to; +}; + +} // namespace traits + +} // namespace sgl diff --git a/include/sgl/registry.hpp b/include/sgl/registry.hpp new file mode 100644 index 00000000..a4ad4e4c --- /dev/null +++ b/include/sgl/registry.hpp @@ -0,0 +1,190 @@ +#pragma once + +#include "gl/attributes/force_inline.hpp" +#include "sgl/constants.hpp" +#include "sgl/sketch_traits.hpp" +#include "sgl/types.hpp" + +#include +#include +#include +#include +#include + +namespace sgl { + +template +class sketch_registry { +public: + using traits_type = Traits; + using id_type = typename traits_type::id_type; + using weight_type = typename traits_type::weight_type; + using edge_sample_type = typename traits_type::edge_sample_type; + + using view_type = vertex_sketch_view; + using const_view_type = const_vertex_sketch_view; + + sketch_registry() : sketch_registry(default_sketch_width) {} + + explicit sketch_registry(const size_type m) + : _m(m), _m_stride(align_sketch_stride(m, sizeof(weight_type))) { + if (m < 2uz) + throw std::invalid_argument("EdgeSketch width m must be >= 2"); + } + + sketch_registry(const sketch_registry&) = default; + sketch_registry(sketch_registry&&) noexcept = default; + sketch_registry& operator=(const sketch_registry&) = default; + sketch_registry& operator=(sketch_registry&&) noexcept = default; + ~sketch_registry() = default; + + [[nodiscard]] gl_attr_force_inline size_type width() const noexcept { + return this->_m; + } + + [[nodiscard]] gl_attr_force_inline size_type stride() const noexcept { + return this->_m_stride; + } + + [[nodiscard]] gl_attr_force_inline size_type n_vertices() const noexcept { + return this->_inv_ids.size(); + } + + [[nodiscard]] gl_attr_force_inline bool has_vertex(const id_type id) const { + return this->_id_map.contains(id); + } + + [[nodiscard]] gl_attr_force_inline const std::vector& vertex_ids() const noexcept { + return this->_inv_ids; + } + + size_type get_or_create(const id_type id) { + if (const auto it = this->_id_map.find(id); it != this->_id_map.end()) + return it->second; + + const auto idx = this->_inv_ids.size(); + this->_id_map.emplace(id, idx); + this->_inv_ids.push_back(id); + this->_grow_to(idx + 1uz); + this->_init_row(idx); + return idx; + } + + [[nodiscard]] size_type index_of(const id_type id) const { + const auto it = this->_id_map.find(id); + if (it == this->_id_map.end()) + throw std::invalid_argument("Vertex id is not present in the sketch registry"); + return it->second; + } + + [[nodiscard]] gl_attr_force_inline id_type id_at(const size_type idx) const { + return this->_inv_ids.at(idx); + } + + [[nodiscard]] view_type view_at(const size_type idx) { + this->_verify_idx(idx); + const auto off = idx * this->_m_stride; + return view_type{ + std::span{this->_s.data() + off, this->_m}, + std::span{this->_f.data() + off, this->_m}, + &this->_max_s[idx] + }; + } + + [[nodiscard]] const_view_type view_at(const size_type idx) const { + this->_verify_idx(idx); + const auto off = idx * this->_m_stride; + return const_view_type{ + std::span{this->_s.data() + off, this->_m}, + std::span{this->_f.data() + off, this->_m}, + &this->_max_s[idx] + }; + } + + [[nodiscard]] gl_attr_force_inline view_type view_of(const id_type id) { + return this->view_at(this->index_of(id)); + } + + [[nodiscard]] gl_attr_force_inline const_view_type view_of(const id_type id) const { + return this->view_at(this->index_of(id)); + } + + void merge_min(const size_type dst_idx, const const_view_type& src) { + auto dst = this->view_at(dst_idx); + if (src.width() != dst.width()) + throw std::invalid_argument("Cannot merge sketches with different widths"); + + bool touched_max = false; + for (size_type j = 0uz; j < dst.width(); ++j) { + if (src.S[j] < dst.S[j]) { + if (dst.S[j] == *dst.max_s) + touched_max = true; + dst.S[j] = src.S[j]; + dst.F[j] = src.F[j]; + } + } + + if (touched_max) + *dst.max_s = this->_row_max(dst.S); + } + + void merge_min(const size_type dst_idx, const size_type src_idx) { + this->merge_min(dst_idx, this->view_at(src_idx)); + } + + void reserve(const size_type n_vertices) { + this->_id_map.reserve(n_vertices); + this->_inv_ids.reserve(n_vertices); + this->_max_s.reserve(n_vertices); + this->_s.reserve(n_vertices * this->_m_stride); + this->_f.reserve(n_vertices * this->_m_stride); + } + + void clear() { + this->_id_map.clear(); + this->_inv_ids.clear(); + this->_s.clear(); + this->_f.clear(); + this->_max_s.clear(); + } + +private: + void _verify_idx(const size_type idx) const { + if (idx >= this->n_vertices()) + throw std::invalid_argument("Sketch row index out of range"); + } + + void _grow_to(const size_type n_vertices) { + this->_max_s.resize(n_vertices); + this->_s.resize(n_vertices * this->_m_stride); + this->_f.resize(n_vertices * this->_m_stride); + } + + void _init_row(const size_type idx) { + const auto off = idx * this->_m_stride; + for (size_type j = 0uz; j < this->_m; ++j) { + this->_s[off + j] = sketch_infinity_v; + this->_f[off + j] = edge_sample_type{}; + } + this->_max_s[idx] = sketch_infinity_v; + } + + [[nodiscard]] static weight_type _row_max(const std::span S) { + weight_type m = S.front(); + for (size_type j = 1uz; j < S.size(); ++j) + m = std::max(m, S[j]); + return m; + } + + size_type _m; + size_type _m_stride; + + std::unordered_map _id_map; + std::vector _inv_ids; + + std::vector _s; + std::vector _f; + std::vector _max_s; +}; + +} // namespace sgl diff --git a/include/sgl/sgl.hpp b/include/sgl/sgl.hpp new file mode 100644 index 00000000..bb31851d --- /dev/null +++ b/include/sgl/sgl.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "sgl/constants.hpp" +#include "sgl/directional_tags.hpp" +#include "sgl/edge_key.hpp" +#include "sgl/hash.hpp" +#include "sgl/registry.hpp" +#include "sgl/sketch.hpp" +#include "sgl/sketch_traits.hpp" +#include "sgl/traits.hpp" +#include "sgl/types.hpp" +#include "sgl/update.hpp" diff --git a/include/sgl/sketch.hpp b/include/sgl/sketch.hpp new file mode 100644 index 00000000..dd55e968 --- /dev/null +++ b/include/sgl/sketch.hpp @@ -0,0 +1,254 @@ +#pragma once + +#include "gl/attributes/force_inline.hpp" +#include "sgl/directional_tags.hpp" +#include "sgl/edge_key.hpp" +#include "sgl/sketch_traits.hpp" +#include "sgl/registry.hpp" +#include "sgl/update.hpp" + +#include +#include +#include +#include + +namespace sgl { + +template > +class sketch; + +namespace traits { + +template +concept c_sketch = c_instantiation_of, sketch>; + +template +concept c_directed_sketch = + c_sketch and c_directed_sketch_traits::traits_type>; + +template +concept c_undirected_sketch = + c_sketch and c_undirected_sketch_traits::traits_type>; + +} // namespace traits + +template +class sketch { +public: + using traits_type = Traits; + using directional_tag = typename traits_type::directional_tag; + using id_type = typename traits_type::id_type; + using weight_type = typename traits_type::weight_type; + using edge_key_policy = typename traits_type::edge_key_policy; + using hash_policy = typename traits_type::hash_policy; + using edge_sample_type = typename traits_type::edge_sample_type; + using key_type = typename traits_type::key_type; + using registry_type = sketch_registry; + using view_type = typename registry_type::view_type; + using const_view_type = typename registry_type::const_view_type; + + sketch() : sketch(default_sketch_width) {} + + explicit sketch(const size_type m) : _registry(m) {} + + sketch(sketch&&) noexcept = default; + sketch& operator=(sketch&&) noexcept = default; + sketch(const sketch&) = default; + sketch& operator=(const sketch&) = default; + ~sketch() = default; + + [[nodiscard]] gl_attr_force_inline size_type width() const noexcept { + return this->_registry.width(); + } + + [[nodiscard]] gl_attr_force_inline size_type n_vertices() const noexcept { + return this->_registry.n_vertices(); + } + + [[nodiscard]] gl_attr_force_inline bool has_vertex(const id_type id) const { + return this->_registry.has_vertex(id); + } + + [[nodiscard]] gl_attr_force_inline const std::vector& vertex_ids() const noexcept { + return this->_registry.vertex_ids(); + } + + [[nodiscard]] gl_attr_force_inline view_type sketch_of(const id_type id) { + return this->_registry.view_of(id); + } + + [[nodiscard]] gl_attr_force_inline const_view_type sketch_of(const id_type id) const { + return this->_registry.view_of(id); + } + + [[nodiscard]] gl_attr_force_inline registry_type& registry() noexcept { + return this->_registry; + } + + [[nodiscard]] gl_attr_force_inline const registry_type& registry() const noexcept { + return this->_registry; + } + + void update_edge(const id_type u, const id_type v, const weight_type weight = weight_type{1}) + requires(traits::c_binary_sketch_traits) + { + const auto key = edge_key_policy::make(u, v); + const edge_sample_type sample{u, v}; + this->_update_edge_binary(u, v, key, weight, sample); + } + + template + void update_edge( + const id_type u, + const id_type v, + const Occurrence occurrence, + const weight_type weight = weight_type{1} + ) + requires( + traits::c_multi_sketch_traits + and std::convertible_to + ) + { + const auto key = edge_key_policy::make( + u, v, static_cast(occurrence) + ); + const edge_sample_type sample{u, v}; + this->_update_edge_binary(u, v, key, weight, sample); + } + + template + void update_edge( + const id_type u, + const id_type v, + const Label& label, + const weight_type weight = weight_type{1} + ) + requires( + traits::c_labeled_sketch_traits + and std::convertible_to + ) + { + const auto key = edge_key_policy::make(u, v, label); + const edge_sample_type sample{u, v}; + this->_update_edge_binary(u, v, key, weight, sample); + } + + void update_edge( + const traits::c_sized_range_of auto& endpoints, + const weight_type weight = weight_type{1} + ) + requires(traits::c_hyper_sketch_traits) + { + const auto key = edge_key_policy::make(endpoints); + if (key.size() < 1uz) + throw std::invalid_argument("Hyperedge must contain at least one endpoint"); + + const auto anchor = key.front(); + for (const auto id : key) { + const auto idx = this->_registry.get_or_create(id); + const edge_sample_type sample{anchor, id}; + detail::update_es(this->_registry.view_at(idx), key, weight, sample); + } + } + + void merge(const sketch& other) { + if (other.width() != this->width()) + throw std::invalid_argument("Cannot merge EdgeSketches with different widths"); + + for (const auto id : other.vertex_ids()) { + const auto dst = this->_registry.get_or_create(id); + this->_registry.merge_min(dst, other._registry.view_of(id)); + } + } + + [[nodiscard]] weight_type estimate_degree(const id_type id) const { + const auto row = this->_registry.view_of(id); + weight_type sum = static_cast(0); + for (const auto s : row.S) { + if (not std::isfinite(s)) + return static_cast(0); + sum += s; + } + if (sum <= static_cast(0)) + return static_cast(0); + return static_cast(row.width() - 1uz) / sum; + } + + void reserve(const size_type n_vertices) { + this->_registry.reserve(n_vertices); + } + + void clear() { + this->_registry.clear(); + } + +private: + template + void _update_edge_binary( + const id_type u, + const id_type v, + const Key& key, + const weight_type weight, + const edge_sample_type& sample + ) { + const auto update_one = [&](const id_type vertex) { + const auto idx = this->_registry.get_or_create(vertex); + detail::update_es(this->_registry.view_at(idx), key, weight, sample); + }; + + if constexpr (traits_type::is_directed) { + update_one(u); + } + else { + update_one(u); + if (u != v) + update_one(v); + } + } + + registry_type _registry; +}; + +template < + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + traits::c_edge_key_policy EdgeKeyPolicy = binary_edge_key, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using undirected_sketch = + sketch>; + +template < + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + traits::c_edge_key_policy EdgeKeyPolicy = binary_edge_key, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using directed_sketch = + sketch>; + +template < + traits::c_sketch_directional_tag DirectionalTag = undirected_t, + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + std::unsigned_integral OccurrenceType = IdType, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using multi_sketch = sketch< + multi_sketch_traits>; + +template < + traits::c_sketch_directional_tag DirectionalTag = undirected_t, + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + typename LabelType = IdType, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using labeled_sketch = sketch< + labeled_sketch_traits>; + +template < + traits::c_sketch_directional_tag DirectionalTag = undirected_t, + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using hyper_sketch = + sketch>; + +} // namespace sgl diff --git a/include/sgl/sketch_traits.hpp b/include/sgl/sketch_traits.hpp new file mode 100644 index 00000000..377d486f --- /dev/null +++ b/include/sgl/sketch_traits.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include "sgl/constants.hpp" +#include "sgl/directional_tags.hpp" +#include "sgl/edge_key.hpp" +#include "sgl/hash.hpp" +#include "sgl/types.hpp" + +namespace sgl { + +template < + traits::c_sketch_directional_tag DirectionalTag = undirected_t, + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + traits::c_edge_key_policy EdgeKeyPolicy = binary_edge_key, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +struct sketch_traits { + using directional_tag = DirectionalTag; + using id_type = IdType; + using weight_type = WeightType; + using edge_key_policy = EdgeKeyPolicy; + using hash_policy = HashPolicy; + + using edge_sample_type = edge_sample; + using key_type = typename edge_key_policy::key_type; + + static constexpr bool is_directed = std::same_as; + static constexpr bool is_undirected = std::same_as; +}; + +template < + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + traits::c_edge_key_policy EdgeKeyPolicy = binary_edge_key, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using undirected_sketch_traits = + sketch_traits; + +template < + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + traits::c_edge_key_policy EdgeKeyPolicy = binary_edge_key, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using directed_sketch_traits = + sketch_traits; + +template < + traits::c_sketch_directional_tag DirectionalTag = undirected_t, + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + std::unsigned_integral OccurrenceType = IdType, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using multi_sketch_traits = sketch_traits< + DirectionalTag, + IdType, + WeightType, + multi_edge_key, + HashPolicy>; + +template < + traits::c_sketch_directional_tag DirectionalTag = undirected_t, + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + typename LabelType = IdType, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using labeled_sketch_traits = sketch_traits< + DirectionalTag, + IdType, + WeightType, + labeled_edge_key, + HashPolicy>; + +template < + traits::c_sketch_directional_tag DirectionalTag = undirected_t, + traits::c_id_type IdType = default_id_type, + traits::c_weight_type WeightType = default_weight_type, + traits::c_edge_hash_policy HashPolicy = default_edge_hash> +using hyper_sketch_traits = sketch_traits< + DirectionalTag, + IdType, + WeightType, + hyper_edge_key, + HashPolicy>; + +namespace traits { + +template +concept c_sketch_traits = c_instantiation_of; + +template +concept c_directed_sketch_traits = + c_sketch_traits + and std::same_as; + +template +concept c_undirected_sketch_traits = + c_sketch_traits + and std::same_as; + +template +concept c_binary_sketch_traits = + c_sketch_traits + and c_binary_edge_key_policy; + +template +concept c_multi_sketch_traits = + c_sketch_traits + and c_multi_edge_key_policy; + +template +concept c_labeled_sketch_traits = + c_sketch_traits + and c_labeled_edge_key_policy; + +template +concept c_hyper_sketch_traits = + c_sketch_traits + and c_hyper_edge_key_policy; + +} // namespace traits + +} // namespace sgl diff --git a/include/sgl/traits.hpp b/include/sgl/traits.hpp new file mode 100644 index 00000000..2ff7f164 --- /dev/null +++ b/include/sgl/traits.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "gl/traits.hpp" + +#include +#include + +namespace sgl { + +namespace traits { + +inline constexpr std::size_t min_hash_word_size = 8uz; + +using gl::traits::c_arithmetic; +using gl::traits::c_forward_range; +using gl::traits::c_forward_range_of; +using gl::traits::c_id_type; +using gl::traits::c_instantiation_of; +using gl::traits::c_one_of; +using gl::traits::c_range; +using gl::traits::c_sized_range; +using gl::traits::c_sized_range_of; +using gl::traits::is_instantiation_of; +using gl::traits::is_instantiation_of_v; + +template +concept c_weight_type = c_arithmetic and std::floating_point; + +template +concept c_hash_word_type = + std::unsigned_integral and (sizeof(T) >= min_hash_word_size); + +template +concept c_uniform_type = std::floating_point; + +} // namespace traits + +} // namespace sgl diff --git a/include/sgl/types.hpp b/include/sgl/types.hpp new file mode 100644 index 00000000..d38d240a --- /dev/null +++ b/include/sgl/types.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "gl/types/core.hpp" +#include "sgl/constants.hpp" +#include "sgl/traits.hpp" + +#include +#include +#include + +namespace sgl { + +using size_type = gl::size_type; + +using default_id_type = gl::default_id_type; + +using gl::to_idx; + +using gl::to_diff; + +template +using homogeneous_pair = gl::homogeneous_pair; + +using default_weight_type = double; + +using default_occurrence_type = default_id_type; + +using default_label_type = default_id_type; + +using default_hash_word_type = std::uint64_t; + +using default_uniform_type = default_weight_type; + +template +struct edge_sample { + using id_type = IdType; + + id_type u = invalid_id_v; + id_type v = invalid_id_v; + + [[nodiscard]] constexpr bool is_valid() const noexcept { + return this->u != invalid_id_v and this->v != invalid_id_v; + } + + [[nodiscard]] constexpr bool operator==(const edge_sample&) const noexcept = default; +}; + +template +struct vertex_sketch_view { + using weight_type = WeightType; + using id_type = IdType; + using edge_sample_type = edge_sample; + + std::span S; + std::span F; + weight_type* max_s = nullptr; + + [[nodiscard]] constexpr size_type width() const noexcept { + return this->S.size(); + } + + [[nodiscard]] constexpr bool valid() const noexcept { + return this->max_s != nullptr and this->S.size() == this->F.size() and not this->S.empty(); + } +}; + +template +struct const_vertex_sketch_view { + using weight_type = WeightType; + using id_type = IdType; + using edge_sample_type = edge_sample; + + std::span S; + std::span F; + const weight_type* max_s = nullptr; + + [[nodiscard]] constexpr size_type width() const noexcept { + return this->S.size(); + } + + [[nodiscard]] constexpr bool valid() const noexcept { + return this->max_s != nullptr and this->S.size() == this->F.size() and not this->S.empty(); + } +}; + +} // namespace sgl diff --git a/include/sgl/update.hpp b/include/sgl/update.hpp new file mode 100644 index 00000000..989c0042 --- /dev/null +++ b/include/sgl/update.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include "sgl/sketch_traits.hpp" +#include "sgl/types.hpp" + +#include +#include +#include +#include + +namespace sgl { +namespace detail { + +template +class fisher_yates_perm { +public: + using seed_type = SeedType; + + explicit fisher_yates_perm(const size_type m, const seed_type seed) + : _perm(m) + , _state(seed == seed_type{} ? static_cast(0x9e3779b97f4a7c15ULL) : seed) { + for (size_type i = 0uz; i < m; ++i) + this->_perm[i] = i; + } + + [[nodiscard]] size_type next_slot(const size_type k) { + const auto remaining = static_cast(this->_perm.size() - k); + const auto r = k + static_cast(this->_next() % remaining); + std::swap(this->_perm[k], this->_perm[r]); + return this->_perm[k]; + } + +private: + [[nodiscard]] seed_type _next() noexcept { + this->_state ^= this->_state >> 12; + this->_state ^= this->_state << 25; + this->_state ^= this->_state >> 27; + return this->_state * static_cast(0x2545F4914F6CDD1DULL); + } + + std::vector _perm; + seed_type _state; +}; + +template +void update_es( + vertex_sketch_view row, + const Key& key, + const typename Traits::weight_type weight, + const edge_sample& sample_to_store +) { + using weight_type = typename Traits::weight_type; + using hash_policy = typename Traits::hash_policy; + using seed_type = typename hash_policy::seed_type; + + if (not row.valid()) + throw std::invalid_argument("Invalid vertex sketch view passed to update_es"); + if (weight <= static_cast(0)) + throw std::invalid_argument("EdgeSketch edge weight must be positive"); + + const auto m = row.width(); + fisher_yates_perm perm(m, hash_policy::seed(key)); + + weight_type sum = static_cast(0); + bool update_max = false; + + for (size_type k = 0uz; k < m; ++k) { + const auto U = static_cast(hash_policy::uniform(key, k + 1uz)); + const auto E = -std::log(U) / weight; + sum += E / static_cast(m - k); + + if (sum >= *row.max_s) + break; + + const auto l = perm.next_slot(k); + if (sum < row.S[l]) { + if (row.S[l] == *row.max_s) + update_max = true; + row.S[l] = sum; + row.F[l] = sample_to_store; + } + } + + if (update_max) { + weight_type mx = row.S[0]; + for (size_type j = 1uz; j < m; ++j) + mx = std::max(mx, row.S[j]); + *row.max_s = mx; + } +} + +} // namespace detail +} // namespace sgl diff --git a/sketch-graph-library.md b/sketch-graph-library.md new file mode 100644 index 00000000..a58663be --- /dev/null +++ b/sketch-graph-library.md @@ -0,0 +1,208 @@ +# Sketch Graph Library + +## Introduction +A C++ engine implementing a compact graph representation for analyzing massive data streams. The architecture eliminates traditional adjacency lists or matrices in favor of compressed node sketches with memory complexity independent of the data size. It is based on the EdgeSketch structure described in the paper **"EdgeSketch: Efficient Analysis of Massive Graph Streams"**. This enables executing graph algorithms directly on the stored summary, without accessing the original graph, which significantly reduces the time complexity of some of them. + +## Goals +* Efficient, multiple-header C++ library for graph sketching. +* Ability to generate a single-header file for easy integration into other projects. +* Support for numerous graph algorithms operating directly on sketches. +* Distributed version for large-scale graph processing. +* Python wrapper for easy integration with data science workflows. + +## EdgeSketch +* Constructed in a fully streaming manner during a single pass over the edge stream. Instead of maintaining the full connection structure, for each node $i$, the system creates and updates a fixed-size sketch $M_i = (F_i, S_i)$, consisting of two complementary arrays of length $m$. +* $S_i$ (Aggregation): Built upon the **FastExpSketch** algorithm, it stores aggregated information about the weights of all incident edges using the minima of exponentially distributed variables. It is used for estimating node degrees, evaluating graph density, and enabling fast set-theoretic operations (e.g., calculating unions or intersections of node sets) in $O(m)$ time. +* $F_i$ (Sampling): Stores an explicit, weighted sample of edges connected to a given node. This component is essential for reconstructing local paths, estimating the proportion of internal edges in subgraphs, and simulating random walks. + +## Graph Types +* **Directed graphs:** Edge $(u, v)$ is hashed as $u || v$ and updates $M_u$. +* **Undirected graphs:** Edge $(u, v)$ is hashed as $\min(u,v) || \max(u,v)$ and updates both $M_u$ and $M_v$. +* **Weighted graphs:** Edge $(u, v)$ with weight $w$ is natively handled by the EdgeSketch algorithm. +* **Hypergraphs:** Edge $(u_1, u_2, \dots, u_k)$ is hashed as $u_1 || u_2 || \dots || u_k$ and updates all $M_{u_i}$. +* **Multigraphs:** Edge $(u, v)$ is hashed as $u || v || \text{counter}$. +* **Labeled graphs:** Edge $(u, v)$ with label $l$ is hashed as $u || v || l$. + +## Algorithms +* Edge count estimation +* Node degree estimation +* Graph density estimation +* Subgraph inclusion check +* Motif counting +* DFS, BFS (approximated) +* Random walks +* Modularity and community detection (e.g., Louvain method) +* Greedy peeling +* Min-cut (Karger's algorithm) +* Minimum spanning tree (MST) +* Edge prediction / Graph reconstruction +* Node similarity +* ... + +## Distributed System +* gRPC + HTTP/2 + Docker +* Lightweight custom message broker +* Ingestors (workers) and aggregators (master nodes) +* Memory-mapped files (`mmap`) instead of standard I/O, optimized with SIMD instructions + +## Python wrapper + +## Technical Details + +### Memory Layout + +```text +BUFFER 1: S values registry +Type: float/double +Alignment: 64 bytes + +Base offset (0x0000...) +| ++--> [ Node 0 ] +| [ S_0 | S_1 | S_2 | S_3 | S_4 | S_5 | S_6 | S_7 ] -> Perfect 1 Cache Line hit (64B) +| [ S_8 | S_9 | ... | S_99] +| ++--> [ Node 1 ] +| [ S_0 | S_1 | S_2 | S_3 | S_4 | S_5 | S_6 | S_7 ] +| [ S_8 | S_9 | ... | S_99] +| +... ++--> [ Node N-1 ] + [ S_0 ... S_99 ] + +BUFFER 2: F samples registry +Type: struct { uint64_t u; uint64_t v; } +Alignment: 64 bytes + +Base offset (0x8000...) +| ++--> [ Node 0 ] +| [ (u,v)_0 | (u,v)_1 | (u,v)_2 | (u,v)_3 ] -> Perfect 1 Cache Line hit (64B) +| [ (u,v)_4 | (u,v)_5 | ... | (u,v)_99] +| ++--> [ Node 1 ] +| [ (u,v)_0 | (u,v)_1 | (u,v)_2 | (u,v)_3 ] +| [ (u,v)_4 | ... | (u,v)_99] +| +... +``` + +### Implementation Example + +```cpp +#include +#include +#include +#include + +// Edge definition (F part of the sketch) +struct Edge { + uint64_t u; + uint64_t v; +}; + +class SketchRegistry { +private: + uint64_t m_size; // Sketch size (e.g., 100) + double* s_values; // Flat buffer: N * m_size + Edge* f_values; // Flat buffer: N * m_size + double* max_s_values; // Flat buffer storing the current maximum in each sketch (FastExpSketch optimization) + + // Custom map: Global NodeID -> Internal Index + // absl::flat_hash_map id_mapping; + +public: + + /** + * @brief Processes a new incident edge and updates sketches. + * + * For undirected graphs, we call this function twice (for u and v) + * or modify it to internally update both pointers. + */ + void ProcessEdge(uint64_t target_node, uint64_t edge_u, uint64_t edge_v, double weight = 1.0) { + uint64_t idx = GetOrCreateInternalIndex(target_node); + + double* local_s = s_values + (idx * m_size); + Edge* local_f = f_values + (idx * m_size); + double& current_max = max_s_values[idx]; + + uint64_t min_node = std::min(edge_u, edge_v); + uint64_t max_node = std::max(edge_u, edge_v); + + SeedRandom(min_node, max_node); + + double sum = 0.0; + bool update_max = false; + + for (uint64_t k = 1; k <= m_size; ++k) { + double E = -log(RandomUniform()) / weight; + sum += E / (m_size - k + 1); + + // Early exit - FastExpSketch optimization + if (sum >= current_max) { + break; + } + + uint64_t l = GetRandomPermutationPosition(); + + if (sum < local_s[l]) { + if (local_s[l] == current_max) update_max = true; + local_s[l] = sum; + local_f[l] = {edge_u, edge_v}; + } + } + + if (update_max) { + current_max = RecalculateMax(local_s, m_size); + } + } + + /** + * @brief Performs a union (merge) of the sketch. + */ + void MergeSketch(uint64_t local_idx, const double* remote_s, const Edge* remote_f) { + double* local_s = s_values + (local_idx * m_size); + Edge* local_f = f_values + (local_idx * m_size); + + bool max_changed = false; + + // Vectorized loop (e.g., AVX-512) + for (uint64_t i = 0; i < m_size; ++i) { + if (remote_s[i] < local_s[i]) { + local_s[i] = remote_s[i]; + local_f[i] = remote_f[i]; + max_changed = true; + } + } + + if (max_changed) { + max_s_values[local_idx] = RecalculateMax(local_s, m_size); + } + } + + /** + * @brief Reads the estimated node degree with an O(1/m) error guarantee. + */ + double EstimateNodeDegree(uint64_t node_id) const { + uint64_t idx = GetInternalIndex(node_id); + const double* local_s = s_values + (idx * m_size); + + double sum_s = 0.0; + for (uint64_t i = 0; i < m_size; ++i) { + sum_s += local_s[i]; + } + + // Unbiased estimator + return (m_size - 1) / sum_s; + } + + /** + * @brief Returns a weighted edge sample (F array). + */ + const Edge* GetEdgeSample(uint64_t node_id) const { + uint64_t idx = GetInternalIndex(node_id); + + return f_values + (idx * m_size); + } +}; +``` \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1e2035c8..8bc40d52 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,3 +57,8 @@ add_test_target(gl "${GL_SOURCE_DIRS}" "${GL_DATA_DIR}" GL_TESTING) set(HGL_SOURCE_DIRS "source/hgl" ${SOURCE_DIRS}) set(HGL_DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/data/hgl") add_test_target(hgl "${HGL_SOURCE_DIRS}" "${HGL_DATA_DIR}" HGL_TESTING) + +# SGL target +set(SGL_SOURCE_DIRS "source/sgl" ${SOURCE_DIRS}) +set(SGL_DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/data/sgl") +add_test_target(sgl "${SGL_SOURCE_DIRS}" "${SGL_DATA_DIR}" SGL_TESTING) diff --git a/tests/include/testing/sgl/constants.hpp b/tests/include/testing/sgl/constants.hpp new file mode 100644 index 00000000..da78d4a6 --- /dev/null +++ b/tests/include/testing/sgl/constants.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#define IC inline constexpr + +namespace sgl_testing::constants { + +IC sgl::size_type sketch_width = 16uz; +IC sgl::size_type invalid_width = 1uz; + +IC sgl::default_id_type v0 = 0u; +IC sgl::default_id_type v1 = 1u; +IC sgl::default_id_type v2 = 2u; +IC sgl::default_id_type v3 = 3u; + +IC sgl::default_weight_type unit_weight = 1.0; +IC sgl::default_weight_type zero_weight = 0.0; + +} // namespace sgl_testing::constants diff --git a/tests/source/sgl/test_sketch.cpp b/tests/source/sgl/test_sketch.cpp new file mode 100644 index 00000000..2024f751 --- /dev/null +++ b/tests/source/sgl/test_sketch.cpp @@ -0,0 +1,193 @@ +#include "doctest.h" +#include "testing/common/functional.hpp" +#include "testing/sgl/constants.hpp" + +#include + +#include +#include +#include + +namespace { + +using namespace sgl_testing::constants; + +template +[[nodiscard]] bool row_max_matches(const Sketch& sut, const typename Sketch::id_type id) { + const auto row = sut.sketch_of(id); + const auto expected = *std::ranges::max_element(row.S); + return *row.max_s == expected; +} + +template +[[nodiscard]] bool any_slot_updated(const Sketch& sut, const typename Sketch::id_type id) { + using weight_type = typename Sketch::weight_type; + const auto row = sut.sketch_of(id); + return std::ranges::any_of(row.S, [](const auto s) { + return s < sgl::sketch_infinity_v; + }); +} + +} // namespace + +TEST_CASE_TEMPLATE_DEFINE( + "common binary sketch tests", TraitsType, common_binary_sketch_traits_template +) { + using sut_type = sgl::sketch; + using weight_type = typename sut_type::weight_type; + + SUBCASE("default construction uses default sketch width and no vertices") { + const sut_type sut; + CHECK_EQ(sut.width(), sgl::default_sketch_width); + CHECK_EQ(sut.n_vertices(), 0uz); + CHECK_FALSE(sut.has_vertex(v0)); + } + + SUBCASE("construction with explicit width") { + const sut_type sut{sketch_width}; + CHECK_EQ(sut.width(), sketch_width); + CHECK_EQ(sut.n_vertices(), 0uz); + } + + SUBCASE("construction with width < 2 throws") { + CHECK_THROWS_AS(sut_type{invalid_width}, std::invalid_argument); + } + + SUBCASE("update_edge inserts endpoints and updates sketch slots") { + sut_type sut{sketch_width}; + sut.update_edge(v0, v1, unit_weight); + + CHECK(sut.has_vertex(v0)); + CHECK(any_slot_updated(sut, v0)); + CHECK(row_max_matches(sut, v0)); + + if constexpr (sut_type::traits_type::is_undirected) { + CHECK(sut.has_vertex(v1)); + CHECK(any_slot_updated(sut, v1)); + CHECK(row_max_matches(sut, v1)); + CHECK_EQ(sut.n_vertices(), 2uz); + } + else { + CHECK_FALSE(sut.has_vertex(v1)); + CHECK_EQ(sut.n_vertices(), 1uz); + } + } + + SUBCASE("update_edge with non-positive weight throws") { + sut_type sut{sketch_width}; + CHECK_THROWS_AS(sut.update_edge(v0, v1, zero_weight), std::invalid_argument); + CHECK_THROWS_AS(sut.update_edge(v0, v1, -unit_weight), std::invalid_argument); + if (sut.has_vertex(v0)) + CHECK_FALSE(any_slot_updated(sut, v0)); + } + + SUBCASE("estimate_degree is positive after updates") { + sut_type sut{sketch_width}; + sut.update_edge(v0, v1, unit_weight); + + CHECK(sut.estimate_degree(v0) > static_cast(0)); + if constexpr (sut_type::traits_type::is_undirected) + CHECK(sut.estimate_degree(v1) > static_cast(0)); + } + + SUBCASE("merge takes elementwise minimum of sketch rows") { + sut_type left{sketch_width}; + sut_type right{sketch_width}; + + left.update_edge(v0, v1, unit_weight); + right.update_edge(v0, v2, unit_weight); + + const auto left_before = left.sketch_of(v0); + std::vector left_s(left_before.S.begin(), left_before.S.end()); + + left.merge(right); + const auto merged = left.sketch_of(v0); + const auto right_row = right.sketch_of(v0); + + for (sgl::size_type j = 0uz; j < sketch_width; ++j) + CHECK_EQ(merged.S[j], std::min(left_s[j], right_row.S[j])); + + CHECK(row_max_matches(left, v0)); + + if constexpr (sut_type::traits_type::is_undirected) + CHECK(left.has_vertex(v2)); + } + + SUBCASE("merge with different widths throws") { + sut_type left{sketch_width}; + sut_type right{sketch_width * 2uz}; + CHECK_THROWS_AS(left.merge(right), std::invalid_argument); + } + + SUBCASE("clear resets vertices") { + sut_type sut{sketch_width}; + sut.update_edge(v0, v1, unit_weight); + REQUIRE(sut.n_vertices() > 0uz); + + sut.clear(); + CHECK_EQ(sut.n_vertices(), 0uz); + CHECK_FALSE(sut.has_vertex(v0)); + } + + SUBCASE("sketch_of for missing vertex throws") { + const sut_type sut{sketch_width}; + CHECK_THROWS_AS(discard(sut.sketch_of(v0)), std::invalid_argument); + } +} + +TEST_CASE_TEMPLATE_INSTANTIATE( + common_binary_sketch_traits_template, + sgl::undirected_sketch_traits<>, + sgl::directed_sketch_traits<> +); + +TEST_CASE("multi sketch update_edge distinguishes occurrences") { + using sut_type = sgl::multi_sketch<>; + + sut_type sut{sketch_width}; + sut.update_edge(v0, v1, 0u, unit_weight); + sut.update_edge(v0, v1, 1u, unit_weight); + + CHECK(sut.has_vertex(v0)); + CHECK(sut.has_vertex(v1)); + CHECK(any_slot_updated(sut, v0)); + CHECK(row_max_matches(sut, v0)); + CHECK(sut.estimate_degree(v0) > 0.0); +} + +TEST_CASE("labeled sketch update_edge accepts labels") { + using sut_type = sgl::labeled_sketch<>; + + sut_type sut{sketch_width}; + sut.update_edge(v0, v1, 42u, unit_weight); + + CHECK(sut.has_vertex(v0)); + CHECK(any_slot_updated(sut, v0)); + CHECK(row_max_matches(sut, v0)); +} + +TEST_CASE("hyper sketch update_edge inserts all endpoints") { + using sut_type = sgl::hyper_sketch<>; + + sut_type sut{sketch_width}; + const std::vector endpoints{v0, v1, v2, v1}; + + sut.update_edge(endpoints, unit_weight); + + CHECK_EQ(sut.n_vertices(), 3uz); + CHECK(sut.has_vertex(v0)); + CHECK(sut.has_vertex(v1)); + CHECK(sut.has_vertex(v2)); + CHECK(any_slot_updated(sut, v0)); + CHECK(any_slot_updated(sut, v1)); + CHECK(any_slot_updated(sut, v2)); + CHECK(row_max_matches(sut, v0)); +} + +TEST_CASE("hyper sketch rejects empty endpoint set") { + using sut_type = sgl::hyper_sketch<>; + + sut_type sut{sketch_width}; + const std::vector empty; + CHECK_THROWS_AS(sut.update_edge(empty, unit_weight), std::invalid_argument); +}