From 138e5580978485242ffaef9da65f29a1c7690778 Mon Sep 17 00:00:00 2001 From: "Myers, Audun D" Date: Wed, 9 Oct 2024 14:30:26 -0400 Subject: [PATCH] adding_HG_matching code, tests, rst, and notebook --- .../source/algorithms/matching_algorithms.rst | 61 ++ hypernetx/algorithms/matching_algorithms.py | 596 ++++++++++++++++++ tests/algorithms/test_matching.py | 180 ++++++ .../Advanced 7 - Matching algorithms.ipynb | 261 ++++++++ 4 files changed, 1098 insertions(+) create mode 100644 docs/source/algorithms/matching_algorithms.rst create mode 100644 hypernetx/algorithms/matching_algorithms.py create mode 100644 tests/algorithms/test_matching.py create mode 100644 tutorials/advanced/Advanced 7 - Matching algorithms.ipynb diff --git a/docs/source/algorithms/matching_algorithms.rst b/docs/source/algorithms/matching_algorithms.rst new file mode 100644 index 00000000..1a85b58f --- /dev/null +++ b/docs/source/algorithms/matching_algorithms.rst @@ -0,0 +1,61 @@ +Matching Algorithms for Hypergraphs +=================================== + +Introduction +------------ +This module implements various algorithms for finding matchings in hypergraphs. These algorithms are based on the methods described in the paper: + +*Distributed Algorithms for Matching in Hypergraphs* by Oussama Hanguir and Clifford Stein. + +The paper addresses the problem of finding matchings in d-uniform hypergraphs, where each hyperedge contains exactly d vertices. The matching problem is NP-complete for d ≥ 3, making it one of the classic challenges in computational theory. The algorithms described here are designed for the Massively Parallel Computation (MPC) model, which is suitable for processing large-scale hypergraphs. + +Mathematical Foundation +------------------------ +The algorithms in this module provide different trade-offs between approximation ratios, memory usage, and computation rounds: + +1. **O(d²)-approximation algorithm**: + - This algorithm partitions the hypergraph into random subgraphs and computes a matching for each subgraph. The results are combined to obtain a matching for the original hypergraph. + - Approximation ratio: O(d²) + - Rounds: 3 + - Memory: O(√nm) + +2. **d-approximation algorithm**: + - Uses sampling and post-processing to iteratively build a maximal matching. + - Approximation ratio: d + - Rounds: O(log n) + - Memory: O(dn) + +3. **d(d−1 + 1/d)²-approximation algorithm**: + - Utilizes the concept of HyperEdge Degree Constrained Subgraphs (HEDCS) to find an approximate matching. + - Approximation ratio: d(d−1 + 1/d)² + - Rounds: 3 + - Memory: O(√nm) for linear hypergraphs, O(n√nm) for general cases. + +These algorithms are crucial for applications that require scalable parallel processing, such as combinatorial auctions, scheduling, and multi-agent systems. + +Usage Example +------------- +Below is an example of how to use the matching algorithms module. + +```python +from hypernetx.algorithms import matching_algorithms as ma + +# Example hypergraph data +hypergraph = ... # Assume this is a d-uniform hypergraph + +# Compute a matching using the O(d²)-approximation algorithm +matching = ma.matching_approximation_d_squared(hypergraph) + +# Compute a matching using the d-approximation algorithm +matching_d = ma.matching_approximation_d(hypergraph) + +# Compute a matching using the d(d−1 + 1/d)²-approximation algorithm +matching_d_squared = ma.matching_approximation_dd(hypergraph) + +print(matching, matching_d, matching_d_squared) + + +References +------------- + +- Oussama Hanguir, Clifford Stein, Distributed Algorithms for Matching in Hypergraphs, https://arxiv.org/pdf/2009.09605 diff --git a/hypernetx/algorithms/matching_algorithms.py b/hypernetx/algorithms/matching_algorithms.py new file mode 100644 index 00000000..bf80b978 --- /dev/null +++ b/hypernetx/algorithms/matching_algorithms.py @@ -0,0 +1,596 @@ +""" +An implementation of the algorithms in: +"Distributed Algorithms for Matching in Hypergraphs", + by Oussama Hanguir and Clifford Stein (2020), https://arxiv.org/abs/2009.09605v1 +Programmer: Shira Rot, Niv +Date: 22.5.2024 +""" + +from functools import lru_cache +import hypernetx as hnx +from hypernetx.classes.hypergraph import Hypergraph +import math +import random +from concurrent.futures import ThreadPoolExecutor + +def approximation_matching_checking(optimal: list, approx: list) -> bool: + """ + Checks if the approximate list contains at least one element that is a subset of each element in the optimal list. + + Parameters + ---------- + optimal : list of lists + A list of lists representing the optimal solutions. + approx : list of lists + A list of lists representing the approximate solutions. + + Returns + ------- + bool + True if the approximate list contains at least one element that is a subset of each element in the optimal list, False otherwise. + """ + for e in optimal: + count = 0 + e_checks = set(e) + for e_m in approx: + e_m_checks = set(e_m) + common_elements = e_checks.intersection(e_m_checks) + checking = bool(common_elements) + if checking: + count += 1 + if count < 1: + return False + return True + + +def greedy_matching(hypergraph: Hypergraph, k: int) -> list: + """ + Greedy algorithm for hypergraph matching. + + This algorithm constructs a random k-partitioning of G and finds a maximal matching. + + Parameters + ---------- + hypergraph : hnx.Hypergraph + A Hypergraph object. + k : int + The number of partitions. + + Returns + ------- + list + The edges of the graph for the greedy matching. + + Raises + ------ + NonUniformHypergraphError + If the hypergraph is not uniform (i.e., if the edges have different sizes). + + Examples + ------- + >>> import numpy as np + >>> np.random.seed(42) + >>> random.seed(42) + >>> edges = {'e1': [1, 2, 3], 'e2': [2, 3, 4], 'e3': [1, 4, 5]} + >>> hypergraph = Hypergraph(edges) + >>> k = 2 + >>> matching = greedy_matching(hypergraph, k) + >>> matching + [(2, 3, 4)] + + >>> np.random.seed(42) + >>> random.seed(42) + >>> edges_large = {f'e{i}': list(range(i, i + 3)) for i in range(1, 50)} + >>> hypergraph_large = Hypergraph(edges_large) + >>> k = 5 + >>> matching_large = greedy_matching(hypergraph_large, k) + >>> len(matching_large) + 12 + + >>> edges_non_uniform = {'e1': [1, 2, 3], 'e2': [4, 5], 'e3': [6, 7, 8, 9]} + >>> hypergraph_non_uniform = Hypergraph(edges_non_uniform) + >>> try: + ... greedy_matching(hypergraph_non_uniform, k) + ... except NonUniformHypergraphError: + ... print("NonUniformHypergraphError raised") + NonUniformHypergraphError raised + """ + + # Check if the hypergraph is empty + if not hypergraph.incidence_dict: + return [] + + # Check if the hypergraph is d-uniform + edge_sizes = {len(edge) for edge in hypergraph.incidence_dict.values()} + if len(edge_sizes) > 1: + raise NonUniformHypergraphError("The hypergraph is not d-uniform.") + + # Partition the hypergraph into k subgraphs + partitions = partition_hypergraph(hypergraph, k) + + # Find maximum matching for each partition in parallel + with ThreadPoolExecutor() as executor: + MM_list = list(executor.map(maximal_matching, partitions)) + + # Initialize the matching set + M = set() + + # Process each partition's matching + for MM_Gi in MM_list: + # Add edges to M if they do not violate the matching property + for edge in MM_Gi: + if not any(set(edge) & set(matching_edge) for matching_edge in M): + M.add(tuple(edge)) + + return list(M) + + +class MemoryLimitExceededError(Exception): + """Custom exception to indicate memory limit exceeded during hypergraph matching.""" + + pass + + +class NonUniformHypergraphError(Exception): + """Custom exception to indicate non d-uniform hypergraph during matching.""" + + pass + + +# necessary because Python's lru_cache decorator +# requires hashable inputs to cache function results. +def edge_tuple(hypergraph): + """ + Converts hypergraph edges to a hashable tuple. + + Parameters + ---------- + hypergraph : hnx.Hypergraph + A Hypergraph object. + + Returns + ------- + tuple + A tuple representing the hypergraph edges, where each element is a tuple containing the edge name and its sorted vertices. + """ + return tuple( + (edge, tuple(sorted(hypergraph.edges[edge]))) + for edge in sorted(hypergraph.edges) + ) + + +@lru_cache(maxsize=None) # to cache the results of this function +def cached_maximal_matching(edges): + """ + Cached version of maximal matching calculation. + + Parameters + ---------- + edges : tuple + A tuple representing the hypergraph edges, where each element is a tuple containing the edge name and its sorted vertices. + + Returns + ------- + list + A list of matching edges. + """ + hypergraph = hnx.Hypergraph( + dict(edges) + ) # Converts the tuple of edges back into a hypergraph. + matching = [] + matched_vertices = set() # vertices that have already been matched. + + for edge in hypergraph.incidence_dict.values(): + if not any( + vertex in matched_vertices for vertex in edge + ): # Checks if current edge is already matched. + matching.append(sorted(edge)) # Adds the current edge to the matching. + matched_vertices.update(edge) + return matching # Returns the list of matching edges. + + +def maximal_matching(hypergraph: Hypergraph) -> list: + """ + Finds a maximal matching in the hypergraph. + + Parameters + ---------- + hypergraph : hnx.Hypergraph + A Hypergraph object. + + Returns + ------- + list + A list of matching edges. + """ + edges = edge_tuple(hypergraph) + return cached_maximal_matching(edges) + + +def sample_edges(hypergraph: Hypergraph, p: float) -> Hypergraph: + """ + Samples edges from the hypergraph with probability p. + + Parameters + ---------- + hypergraph : hnx.Hypergraph + The input hypergraph. + p : float + The probability of sampling each edge. + + Returns + ------- + hnx.Hypergraph + A new hypergraph containing the sampled edges. + """ + sampled_edges = [ + edge for edge in hypergraph.incidence_dict.values() if random.random() < p + ] + return hnx.Hypergraph( + {f"e{i}": tuple(edge) for i, edge in enumerate(sampled_edges)} + ) + + +def sampling_round(S: Hypergraph, p: float, s: int) -> tuple: + """ + Performs a single sampling round on the hypergraph. + + Parameters + ---------- + hypergraph : hnx.Hypergraph + The input hypergraph. + p : float + The probability of sampling each edge. + s : int + The maximum number of edges to include in the matching. + + Returns + ------- + tuple + A tuple containing the maximal matching and the sampled hypergraph. If the sampled hypergraph has more than s edges, None and the sampled hypergraph are returned. + """ + E_prime = sample_edges(S, p) + if len(E_prime.incidence_dict.values()) > s: + return None, E_prime + matching = maximal_matching(E_prime) + return matching, E_prime + + +def iterated_sampling( + hypergraph: Hypergraph, s: int, max_iterations: int = 100 +) -> list: + """ + Iterated Sampling for Hypergraph Matching. + + Uses iterated sampling to find a maximal matching in a d-uniform hypergraph. + + Parameters + ---------- + hypergraph : hnx.Hypergraph + A Hypergraph object. + s : int + The amount of memory available for the computer. + max_iterations : int, optional + The maximum number of iterations to perform. Defaults to 100. + + Returns + ------- + list + The edges of the graph for the approximate matching. + + Raises + ------ + MemoryLimitExceededError + If the memory limit is exceeded during the matching process. + + Examples + ------- + >>> import numpy as np + >>> np.random.seed(42) + >>> random.seed(42) + >>> hypergraph = Hypergraph({0: (1, 2, 3), 1: (2, 3, 4), 2: (3, 4, 5)}) + >>> result = iterated_sampling(hypergraph, 1) + >>> result + [[2, 3, 4]] + + >>> np.random.seed(42) + >>> random.seed(42) + >>> hypergraph = Hypergraph({0: (1, 2, 3, 4), 1: (2, 3, 4, 5), 2: (3, 4, 5, 6)}) + >>> result = iterated_sampling(hypergraph, 2) + >>> result + [[2, 3, 4, 5]] + + >>> np.random.seed(42) + >>> random.seed(42) + >>> hypergraph = Hypergraph({0: (1, 2, 3), 1: (4, 5, 6)}) + >>> result = None + >>> try: + ... result = iterated_sampling(hypergraph, 0) # Insufficient memory, expect failure + ... except MemoryLimitExceededError: + ... pass + >>> result is None + True + + >>> np.random.seed(42) + >>> random.seed(42) + >>> hypergraph = Hypergraph({0: (1, 2, 3), 1: (4, 5, 6)}) + >>> result = iterated_sampling(hypergraph, 10) # Large enough memory, expect a result + >>> result + [[4, 5, 6], [1, 2, 3]] + + >>> np.random.seed(42) + >>> random.seed(42) + >>> hypergraph = Hypergraph({0: (1, 2, 3), 1: (2, 3, 4), 2: (3, 4, 5), 3: (5, 6, 7), 4: (6, 7, 8), 5: (7, 8, 9)}) + >>> result = iterated_sampling(hypergraph, 3) + >>> result + [[2, 3, 4], [5, 6, 7]] + + >>> np.random.seed(42) + >>> random.seed(42) + >>> s = 10 + >>> edges_d4 = {'e1': [1, 2, 3, 4], 'e2': [2, 3, 4, 5], 'e3': [3, 4, 5, 6], 'e4': [4, 5, 6, 7]} + >>> hypergraph_d4 = Hypergraph(edges_d4) + >>> approximate_matching_d4 = iterated_sampling(hypergraph_d4, s) + >>> approximate_matching_d4 + [[2, 3, 4, 5]] + + >>> edges_d5 = {'e1': [1, 2, 3, 4, 5], 'e2': [2, 3, 4, 5, 6], 'e3': [3, 4, 5, 6, 7]} + >>> hypergraph_d5 = Hypergraph(edges_d5) + >>> approximate_matching_d5 = iterated_sampling(hypergraph_d5, s) + >>> approximate_matching_d5 + [[1, 2, 3, 4, 5]] + + >>> edges_d6 = {'e1': [1, 2, 3, 4, 5, 6], 'e2': [2, 3, 4, 5, 6, 7], 'e3': [3, 4, 5, 6, 7, 8]} + >>> hypergraph_d6 = Hypergraph(edges_d6) + >>> approximate_matching_d6 = iterated_sampling(hypergraph_d6, s) + >>> approximate_matching_d6 + [[1, 2, 3, 4, 5, 6]] + + >>> edges_large = {f'e{i}': [i, i + 1, i + 2] for i in range(1, 101)} + >>> hypergraph_large = Hypergraph(edges_large) + >>> approximate_matching_large = iterated_sampling(hypergraph_large, s) + >>> len(approximate_matching_large) + 26 + """ + + d = max((len(edge) for edge in hypergraph.incidence_dict.values()), default=0) + M = [] + S = hypergraph + p = s / (5 * len(S.edges) * d) if len(S.edges) > 0 else 0 + iterations = 0 + + while iterations < max_iterations: + iterations += 1 + M_prime, E_prime = sampling_round(S, p, s) + if M_prime is None: + raise MemoryLimitExceededError( + "Memory limit exceeded during hypergraph matching" + ) + + M.extend(M_prime) + unmatched_vertices = set(S.nodes) - set(v for edge in M_prime for v in edge) + induced_edges = [ + edge + for edge in S.incidence_dict.values() + if all(v in unmatched_vertices for v in edge) + ] + if len(induced_edges) <= s: + M.extend( + maximal_matching( + hnx.Hypergraph( + {f"e{i}": tuple(edge) for i, edge in enumerate(induced_edges)} + ) + ) + ) + break + S = hnx.Hypergraph( + {f"e{i}": tuple(edge) for i, edge in enumerate(induced_edges)} + ) + p = s / (5 * len(S.edges) * d) if len(S.edges) > 0 else 0 + + if iterations >= max_iterations: + raise MemoryLimitExceededError( + "Max iterations reached without finding a solution" + ) + + return M + + +def check_beta_condition(beta, beta_minus, d): + """ + Checks if the beta condition is satisfied. + + Parameters + ---------- + beta : int + The current beta value. + beta_minus : int + The previous beta value. + d : int + The degree of the hypergraph. + + Returns + ------- + bool + True if the beta condition is satisfied, False otherwise. + """ + return (beta - beta_minus) >= (d - 1) + + +def build_HEDCS(hypergraph, beta, beta_minus): + """ + Constructs a Hyper-Edge Degree Constrained Subgraph (HEDCS) from the given hypergraph. + + Parameters + ---------- + hypergraph : hnx.Hypergraph + The input hypergraph. + beta : int + Degree threshold for adding edges. + beta_minus : int + Complementary degree threshold for adding edges. + + Returns + ------- + hnx.Hypergraph + The constructed HEDCS. + """ + H = hnx.Hypergraph(hypergraph.incidence_dict) # Initialize H to be equal to G + degrees = {node: 0 for node in hypergraph.nodes} # Initialize vertex degrees + + for edge in H.edges: + for node in H.edges[edge]: + degrees[node] += 1 + + while True: + violating_edge = None + for edge in list(H.edges): + edge_degree_sum = sum(degrees[node] for node in H.edges[edge]) + if edge_degree_sum > beta: + violating_edge = edge + H.remove_edge(violating_edge) + for node in H.edges[violating_edge]: + degrees[node] -= 1 + break + + for edge in list(hypergraph.edges): + if edge not in H.edges: + edge_degree_sum = sum(degrees[node] for node in hypergraph.edges[edge]) + if edge_degree_sum < beta_minus: + violating_edge = edge + H.add_edge(violating_edge, hypergraph.edges[violating_edge]) + for node in H.edges[violating_edge]: + degrees[node] += 1 + break + + if violating_edge is None: + break + return H + + +def partition_hypergraph(hypergraph, k): + """ + Partitions a hypergraph into k approximately equal-sized subgraphs. + + Parameters + ---------- + hypergraph : hnx.Hypergraph + The input hypergraph. + k : int + The number of partitions. + + Returns + ------- + list[hnx.Hypergraph] + A list of k partitioned hypergraphs. + """ + edges = list(hypergraph.incidence_dict.items()) + random.shuffle(edges) + partitions = [edges[i::k] for i in range(k)] + return [hnx.Hypergraph(dict(part)) for part in partitions] + + +def HEDCS_matching(hypergraph: Hypergraph, s: int) -> list: + """ + HEDCS-Matching for Approximate Hypergraph Matching. + + This algorithm constructs Hyper-Edge Degree Constrained Subgraphs (HEDCS) + to find an approximate maximal matching in a d-uniform hypergraph. It leverages + parallelization to efficiently handle larger hypergraphs. + + Parameters + ---------- + hypergraph : Hypergraph + The input hypergraph. + s : int + The amount of memory available per machine. + + Returns + ------- + list + The edges of the graph for the approximate matching. + + Raises + ------- + NonUniformHypergraphError + If the hypergraph is not d-uniform (all edges don't have the same size). + ValueError + If the calculated beta and beta_minus values do not satisfy the beta condition. + + Examples + ------- + >>> import numpy as np + >>> np.random.seed(42) + >>> random.seed(42) + >>> hypergraph = Hypergraph({0: (1, 2)}) + >>> result = HEDCS_matching(hypergraph, 10) + >>> result + [[1, 2]] + + >>> np.random.seed(42) + >>> random.seed(42) + >>> hypergraph = Hypergraph({0: (1, 2), 1: (3, 4)}) + >>> result = HEDCS_matching(hypergraph, 10) + >>> result + [[1, 2], [3, 4]] + + >>> np.random.seed(42) + >>> random.seed(42) + >>> edges = {'e1': [1, 2, 3], 'e2': [2, 3, 4], 'e3': [1, 4, 5]} + >>> hypergraph = Hypergraph(edges) + >>> s = 10 + >>> approximate_matching = HEDCS_matching(hypergraph, s) + >>> approximate_matching + [[1, 2, 3]] + + >>> np.random.seed(42) + >>> random.seed(42) + >>> edges_large = {f'e{i}': [i, i + 1, i + 2] for i in range(1, 101)} + >>> hypergraph_large = Hypergraph(edges_large) + >>> approximate_matching_large = HEDCS_matching(hypergraph_large, s) + >>> len(approximate_matching_large) + 34 + """ + + edge_sizes = {len(edge) for edge in hypergraph.incidence_dict.values()} + if len(edge_sizes) > 1: + raise NonUniformHypergraphError("The hypergraph is not d-uniform.") + + d = next(iter(edge_sizes)) + n = len(hypergraph.nodes) + m = len(hypergraph.edges) + + beta = 500 * d * 3 * n * 2 * (math.log(n) * 3) + gamma = 1 / (2 * n * math.log(n)) + k = math.ceil(m / (s * math.log(n))) + beta_minus = (1 - gamma) * beta + + if not check_beta_condition(beta, beta_minus, d): + raise ValueError(f"beta - beta_minus must be >= {d - 1}") + + # Partition the hypergraph + partitions = partition_hypergraph(hypergraph, k) + + # Build HEDCS for each partition in parallel + with ThreadPoolExecutor() as executor: + HEDCS_list = list( + executor.map(lambda part: build_HEDCS(part, beta, beta_minus), partitions) + ) + + # Combine all the edges from the HEDCS subgraphs + combined_edges = {} + for H in HEDCS_list: + combined_edges.update(H.incidence_dict) + + combined_hypergraph = hnx.Hypergraph(combined_edges) + + # Find the maximum matching in the combined hypergraph + max_matching = maximal_matching(combined_hypergraph) + + return max_matching + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/tests/algorithms/test_matching.py b/tests/algorithms/test_matching.py new file mode 100644 index 00000000..4b38fc7f --- /dev/null +++ b/tests/algorithms/test_matching.py @@ -0,0 +1,180 @@ +""" +An implementation of the algorithms in: +"Distributed Algorithms for Matching in Hypergraphs", by Oussama Hanguir and Clifford Stein (2020), https://arxiv.org/abs/2009.09605v1 +Programmer: Shira Rot, Niv +Date: 22.5.2024 +""" + +import pytest +from hypernetx.classes.hypergraph import Hypergraph +from hypernetx.algorithms.matching_algorithms import ( + greedy_matching, + HEDCS_matching, + MemoryLimitExceededError, + approximation_matching_checking, +) +from hypernetx.algorithms.matching_algorithms import iterated_sampling + + +def test_greedy_d_approximation_empty_input(): + """ + Test for an empty input hypergraph. + """ + k = 2 + empty_hypergraph = Hypergraph({}) + assert greedy_matching(empty_hypergraph, k) == [] + + +def test_greedy_d_approximation_small_inputs(): + """ + Test for small input hypergraphs. + """ + k = 2 + hypergraph_1 = Hypergraph({"e1": {1, 2, 3}, "e2": {4, 5, 6}}) + assert greedy_matching(hypergraph_1, k) == [(1, 2, 3), (4, 5, 6)] + + hypergraph_2 = Hypergraph( + { + "e1": {1, 2, 3}, + "e2": {4, 5, 6}, + "e3": {7, 8, 9}, + "e4": {1, 4, 7}, + "e5": {2, 5, 8}, + "e6": {3, 6, 9}, + } + ) + result = greedy_matching(hypergraph_2, k) + assert len(result) == 3 + assert all(edge in [(1, 2, 3), (4, 5, 6), (7, 8, 9)] for edge in result) + + +def test_greedy_d_approximation_large_input(): + """ + Test for a large input hypergraph. + """ + k = 2 + large_hypergraph = Hypergraph( + {f"e{i}": {i, i + 1, i + 2} for i in range(1, 100, 3)} + ) + result = greedy_matching(large_hypergraph, k) + assert len(result) == len(large_hypergraph.edges) + assert all(edge in [(i, i + 1, i + 2) for i in range(1, 100, 3)] for edge in result) + + +def test_iterated_sampling_single_edge(): + """ + Test for a hypergraph with a single edge. + It checks if the result is not None and if all edges in the result have at least 2 vertices. + """ + hypergraph = Hypergraph({0: (1, 2, 3)}) + result = iterated_sampling(hypergraph, 10) + assert result is not None and all(len(edge) >= 2 for edge in result) + + +def test_iterated_sampling_two_disjoint_edges(): + """ + Test for a hypergraph with two disjoint edges. + It checks if the result is not None and if all edges in the result have at least 2 vertices. + """ + hypergraph = Hypergraph({0: (1, 2), 1: (3, 4)}) + result = iterated_sampling(hypergraph, 10) + assert result is not None and all(len(edge) >= 2 for edge in result) + + +def test_iterated_sampling_insufficient_memory(): + """ + Test for a hypergraph with insufficient memory. + It checks if the function raises a MemoryLimitExceededError when memory is set to 0. + """ + hypergraph = Hypergraph({0: (1, 2, 3)}) + with pytest.raises(MemoryLimitExceededError): + iterated_sampling(hypergraph, 0) + + +def test_iterated_sampling_large_memory(): + """ + Test for a hypergraph with sufficient memory. + It checks if the result is not None when memory is set to 10. + """ + hypergraph = Hypergraph({0: (1, 2, 3), 1: (4, 5, 6)}) + result = iterated_sampling(hypergraph, 10) + assert result is not None + + +def test_iterated_sampling_max_iterations(): + """ + Test for a hypergraph reaching maximum iterations. + """ + hypergraph = Hypergraph( + { + 0: (1, 2, 3), + 1: (2, 3, 4), + 2: (3, 4, 5), + 3: (5, 6, 7), + 4: (6, 7, 8), + 5: (7, 8, 9), + } + ) + result = iterated_sampling(hypergraph, 3) + assert result is None or all(len(edge) >= 2 for edge in result) + + +def test_iterated_sampling_large_hypergraph(): + """ + Test for a large hypergraph. + """ + edges_large = {f"e{i}": [i, i + 1, i + 2] for i in range(1, 101)} + hypergraph_large = Hypergraph(edges_large) + optimal_matching_large = [edges_large[f"e{i}"] for i in range(1, 101, 3)] + result = iterated_sampling(hypergraph_large, 10) + assert result is not None and approximation_matching_checking( + optimal_matching_large, result + ) + + +def test_HEDCS_matching_single_edge(): + """ + Test for a hypergraph with a single edge. + """ + hypergraph = Hypergraph({0: (1, 2)}) + result = HEDCS_matching(hypergraph, 10) + assert result is not None and all(len(edge) >= 2 for edge in result) + + +def test_HEDCS_matching_two_edges(): + """ + Test for a hypergraph with two disjoint edges. + """ + hypergraph = Hypergraph({0: (1, 2), 1: (3, 4)}) + result = HEDCS_matching(hypergraph, 10) + assert result is not None and all(len(edge) >= 2 for edge in result) + + +def test_HEDCS_matching_with_optimal_matching(): + """ + Test with a hypergraph where the optimal matching is known. + """ + edges = {"e1": [1, 2, 3], "e2": [2, 3, 4], "e3": [1, 4, 5]} + hypergraph = Hypergraph(edges) + s = 10 + optimal_matching = [[1, 2, 3]] # Assuming we know the optimal matching + approximate_matching = HEDCS_matching(hypergraph, s) + assert approximation_matching_checking(optimal_matching, approximate_matching) + + +def test_HEDCS_matching_large_hypergraph(): + """ + Test with a larger hypergraph. + """ + edges_large = {f"e{i}": [i, i + 1, i + 2] for i in range(1, 101)} + hypergraph_large = Hypergraph(edges_large) + s = 10 + optimal_matching_large = [edges_large[f"e{i}"] for i in range(1, 101, 3)] + approximate_matching_large = HEDCS_matching(hypergraph_large, s) + assert approximation_matching_checking( + optimal_matching_large, approximate_matching_large + ) + + +if __name__ == "__main__": + pytest.main() diff --git a/tutorials/advanced/Advanced 7 - Matching algorithms.ipynb b/tutorials/advanced/Advanced 7 - Matching algorithms.ipynb new file mode 100644 index 00000000..9146655d --- /dev/null +++ b/tutorials/advanced/Advanced 7 - Matching algorithms.ipynb @@ -0,0 +1,261 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hypergraph Matching Algorithms Tutorial\n", + "\n", + "This tutorial highlights the implementation and usage of several hypergraph matching algorithms as presented in our publication: [Distributed Algorithms for Matching in Hypergraphs](https://arxiv.org/abs/2009.09605v1).\n", + "\n", + "## Algorithms Covered\n", + "- Greedy Matching\n", + "- Iterated Sampling\n", + "- HEDCS Matching\n", + "\n", + "We will demonstrate how to use these algorithms with example hypergraphs and compare their performance." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import hypernetx as hnx\n", + "from hypernetx.classes.hypergraph import Hypergraph\n", + "from hypernetx.algorithms.matching_algorithms import greedy_matching, iterated_sampling, HEDCS_matching\n", + "import random\n", + "import logging\n", + "import time\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example Hypergraph" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Example hypergraph data\n", + "hypergraph_data = {\n", + " 0: (1, 2, 3),\n", + " 1: (4, 5, 6),\n", + " 2: (7, 8, 9),\n", + " 3: (1, 4, 7),\n", + " 4: (2, 5, 8),\n", + " 5: (3, 6, 9)\n", + "}\n", + "\n", + "# Creating a Hypergraph\n", + "hypergraph = Hypergraph(hypergraph_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Greedy Matching Algorithm\n", + "The Greedy Matching algorithm constructs a random k-partitioning of the hypergraph and finds a maximal matching. \n", + "\n", + "### Parameters:\n", + "- `hypergraph`: The input hypergraph.\n", + "- `k`: The number of partitions to divide the hypergraph into.\n", + "\n", + "### Example Usage:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Greedy Matching Result: [(7, 8, 9), (1, 2, 3), (4, 5, 6)]\n" + ] + } + ], + "source": [ + "k = 3\n", + "greedy_result = greedy_matching(hypergraph, k)\n", + "print(\"Greedy Matching Result:\", greedy_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Iterated Sampling Algorithm\n", + "The Iterated Sampling algorithm uses sampling to find a maximal matching in a d-uniform hypergraph. \n", + "\n", + "### Parameters:\n", + "- `hypergraph`: The input hypergraph.\n", + "- `s`: The number of samples to use in the algorithm.\n", + "\n", + "### Example Usage:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iterated Sampling Result: [[7, 8, 9], [1, 2, 3], [4, 5, 6]]\n" + ] + } + ], + "source": [ + "s = 10\n", + "iterated_result = iterated_sampling(hypergraph, s)\n", + "print(\"Iterated Sampling Result:\", iterated_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## HEDCS Matching Algorithm\n", + "The HEDCS Matching algorithm constructs a Hyper-Edge Degree Constrained Subgraph (HEDCS) to find a maximal matching. \n", + "\n", + "### Parameters:\n", + "- `hypergraph`: The input hypergraph.\n", + "- `s`: The number of samples to use in the algorithm.\n", + "\n", + "### Example Usage:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HEDCS Matching Result: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]\n" + ] + } + ], + "source": [ + "hedcs_result = HEDCS_matching(hypergraph, s)\n", + "print(\"HEDCS Matching Result:\", hedcs_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Performance Comparison\n", + "We will compare the performance of the algorithms on large random hypergraphs." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA04AAAIjCAYAAAA0vUuxAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACet0lEQVR4nOzdd3gUVd/G8e+mJ4SEnlACCYROAAGRIh2NiDSlioQmShVERFAfKSIISkfB8tAUpCmgoICU0IsgICK9S68JCaTuvn/sw74sCZCEJJNyf65rLzJnZ2Z/WzLsnXPmjMlisVgQERERERGRh3IwugAREREREZGMTsFJRERERETkMRScREREREREHkPBSURERERE5DEUnERERERERB5DwUlEREREROQxFJxEREREREQeQ8FJRERERETkMRScREREREREHkPBScQAn332GcWLF8fR0ZHKlSsbXY5kcqGhoZhMJkJDQ40u5YnExcUxePBg/Pz8cHBwoGXLlkaXlC2dPn0ak8nE559/bnQpaeLe78uSJUseu26XLl3w9/dP+6Ke0OzZszGZTJw+fdqQx0/OMSg5r79IRqPgJML//6dz7+bm5kapUqXo27cvly9fTtXHWrNmDYMHD6Z27drMmjWL0aNHp+r+s6vQ0FBefvllfH19cXFxoUCBAjRr1oyffvrJ6NIkiWbOnMlnn31G69atmTNnDm+//fZD161fvz4VKlRI9L6s/sU/o7v3xdhkMvH9998nuk7t2rUxmUwPfQ8f58svv2T27NlPUGXmUr16dUwmE9OnTze6lCSbP38+kyZNMroMkVTlZHQBIhnJyJEjCQgIICoqii1btjB9+nR+/fVX/v77bzw8PFLlMdavX4+DgwP//e9/cXFxSZV9ZnfDhg1j5MiRlCxZkjfffJNixYpx/fp1fv31V1555RXmzZvHq6++anSZaaZu3brcvXs303+e1q9fT+HChZk4caLRpUgqcHNzY/78+bz22mt27adPn2bbtm24ubmleN9ffvkl+fLlo0uXLk9Y5aN98803mM3mNH2Mxzl27Bh//PEH/v7+zJs3j169ehlaT2ISOwbNnz+fv//+mwEDBhhXmEgqU3ASuU+TJk2oVq0aAK+//jp58+ZlwoQJLF++nA4dOjzRvu/cuYOHhwdXrlzB3d091b7kWiwWoqKicHd3T5X9ZTZLlixh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7Pd2zJgxFpPJZDlz5kyCffTr18/WZjabLU2bNrW4uLhYrl69arFY/v/znzdvXsuNGzds6977ffvll18SPN797n0eFy9ebFmxYoXFZDJZzp49a7FYLJZ3333XdjxJ7P1O6jGofPnylnr16iVYNynHnHv1lS1b1hIdHW27f/LkyRbAcuDAAVtb586d7Y6TyXltgoKCLEWKFLHcvn3b1hYaGmoBEhx7H6Zv374WPz8/W+1r1qyxAJa9e/farXfv/7BTp05ZLBaL5dKlSxYnJydLy5Yt7dYbPny4BbD7zA8YMMAC2B2Db9++bQkICLD4+/vbjk/JOQZZLBZL06ZNE32eyXn97/3uzJ8/39Z2+PBhC2BxcHCw7Nixw9a+evXqBL833t7elj59+iSoQSSlNFRP5D6NGzcmf/78+Pn50b59ezw9PVm6dCmFCxfmp59+wmw207ZtW65du2a7+fr6UrJkSTZs2GC3L1dXV7p27Zqkx/31118BGDhwoF37O++8A5BgyERAQADBwcGJ7iskJMTur8HPPPMMFouFbt262a33zDPPcO7cOeLi4mxt958nFRYWxrVr16hXrx4nT560G+IFUK5cOerUqWNbzp8/P6VLl+bkyZO2th9//JFKlSol+IslgMlkAqx/LS9btixlypSxe13vDZN88HW9X3h4OECSepvA+jr7+vrana/m7OzMW2+9RUREBBs3brRbv02bNrbeGbC+ZgCvvfYaTk5Odu0xMTGcP3/ebvtChQrZPXcvLy9CQkLYu3cvly5dAqyfEwcH66E4Pj6e69ev4+npSenSpRMdTtK5c+fHns92r+bVq1dz586dh74WkPTPXFLe74c9jqOjI2+99VaCx7FYLPz222+P3P5R/P39+f333xPcEpvJrW3btri5uTFv3jxb2+rVq7l27VqCyQuABD2s/fr1sz0fIFWOB6tWrcLZ2ZkePXrY2hwcHBI89v0Smxjg/s9DZGQk165do1atWlgsFvbu3Ztg/b59+9p+NplM9O3bl5iYmATDnNq1a0fu3Llty/fe/8e95/d7/vnnyZMnDwsWLMBisbBgwYJHni+anGNQYpJyzLmna9eudr0lyXl+j3ttLly4wIEDBwgJCbHraalXrx5BQUGP3T9YexgXLlxIu3btbLXfG8Z4/+c4MevWrSMuLo7evXvbtd/7HN/v119/pXr16jz77LO2Nk9PT9544w1Onz7NP//8Y7d+Uo5BSZHU19/T05P27dvblkuXLk2uXLkoW7as7ZgM/398vn/7XLlysXPnTi5cuPDE9YqAhuqJ2Pniiy8oVaoUTk5O+Pj4ULp0aduX2mPHjmGxWChZsmSi2z44dKVw4cJJngDizJkzODg4EBgYaNfu6+tLrly5OHPmjF17QEDAQ/dVtGhRu+V7X6L9/PwStJvNZsLCwmxDEbdu3cqwYcPYvn17gi/cYWFhdiHiwccByJ07t90wqRMnTvDKK688tFawvq6HDh0if/78id5/5cqVh27r5eUFWMexJ8WZM2coWbKk7T29596QsAdf5+S8lpBwiFhgYGCCL2ulSpUCrOdK+Pr6YjabmTx5Ml9++SWnTp0iPj7etu699+V+j3rv719n4MCBTJgwgXnz5lGnTh2aN2/Oa6+9Zqs1uZ+5pLzfiTlz5gyFChVKEG4f9ponR44cOWjcuHGC9sSuZZMrVy6aNWvG/Pnz+fjjjwHrML3ChQvbQvr9Hvw9L1GiBA4ODrZ9p8bx4MyZMxQsWDDBJBEPvif3ODk5UaRIkQTtZ8+e5aOPPuLnn39O8H48GDYcHBwoXry4Xdv9n8n7Pfie3wsKyZnQwdnZmTZt2jB//nyqV6/OuXPnHjnDZXKOQYlJyjHnnid5fo/b9t7nOrH3MjAwMEnn2KxZs4arV69SvXp1u3N/GjRowA8//MDYsWMTHMvuedjj58mTxy7w3Vv3/gByz/2/o/dPG5+UY1BSJPX1L1KkSILjqLe3d5KOw+PGjaNz5874+flRtWpVXnzxRUJCQhL8DogklYKTyH2qV69um1XvQWazGZPJxG+//ZboSc0Pjt9OyV/kHvzP4WEete/EantUu+V/J4WfOHGCRo0aUaZMGSZMmICfnx8uLi78+uuvTJw4McGUvI/bX1KZzWaCgoKYMGFCovc/+J/j/cqUKQPAgQMHkvWYSZXS1zI5Ro8ezX/+8x+6devGxx9/TJ48eXBwcGDAgAGJToOc1M/V+PHj6dKlC8uXL2fNmjW89dZbtvN17v/yndTPXGo+Z6OEhISwePFitm3bRlBQED///DO9e/d+6JfP+z34OqXH8eBB9/dO3hMfH89zzz3HjRs3eO+99yhTpgw5cuTg/PnzdOnS5Ymm0k6t9/zVV19lxowZDB8+nEqVKlGuXLlE10vuMehJPcnzS4/fh3u9Sm3btk30/o0bN9KgQYNUe7ykSq0ZXJP6Gj7Jcbht27bUqVOHpUuXsmbNGj777DPGjh3LTz/9RJMmTVJYuWRnCk4iSVSiRAksFgsBAQG2v9CmlmLFimE2mzl27JjdCfGXL1/m1q1bFCtWLFUfLzG//PIL0dHR/Pzzz3Z/CXzUULnHKVGiBH///fdj19m/fz+NGjVK8pf4e0qVKkXp0qVZvnw5kydPTvBl9UHFihXjr7/+wmw2230BPXz4sO3+1HT8+HEsFovd8zp69CiAbYapJUuW0KBBA/773//abXvr1i27E9lTIigoiKCgID788EO2bdtG7dq1mTFjBqNGjUq3z1yxYsVYu3Ytt2/ftut1SqvX/FFeeOEF8ufPz7x583jmmWe4c+cOnTp1SnTdY8eO2f1l/fjx45jNZtv7lhrHg2LFirFhw4YEU5M/OLPYoxw4cICjR48yZ84cQkJCbO2///57ouubzWZOnjxpV/ODn8nU9uyzz1K0aFFCQ0MZO3bsQ9dLzjHoYceKpBxz0sO9z3Vi72VS3t/IyEiWL19Ou3btaN26dYL733rrLebNm/fQ4HT/49//Ob5+/XqCHp1ixYpx5MiRBPt40t/R5B7P00rBggXp3bs3vXv35sqVK1SpUoVPPvlEwUlSROc4iSTRyy+/jKOjIyNGjEjwFzGLxcL169dTvO8XX3wRIMFV1u/1wjRt2jTF+06qe3+9u/+5hYWFMWvWrBTv85VXXmH//v0JZmi7/3Hatm3L+fPn+eabbxKsc/fuXSIjIx/5GCNGjOD69eu8/vrrdudr3bNmzRpWrFgBWF/nS5cusXDhQtv9cXFxTJ06FU9PT+rVq5es5/c4Fy5csHvu4eHhzJ07l8qVK+Pr6wtYX/cHP0+LFy9OcL5UcoSHhyd4LYKCgnBwcLBNNZ5en7kXX3yR+Ph4pk2bZtc+ceJETCZTun55cXJyokOHDixatIjZs2cTFBRExYoVE133iy++sFueOnUqgK3e1DgeBAcHExsba/fZN5vNCR77URL7vbVYLEyePPmh29z/XlgsFqZNm4azszONGjVK8uMmh8lkYsqUKQwbNuyhQRWSdwzKkSMHt27dStCelGNOeihUqBAVKlRg7ty5RERE2No3btyYpB7ypUuXEhkZSZ8+fWjdunWC20svvcSPP/740Es2NGrUCCcnpwRTuT/4ewjW39Fdu3axfft2W1tkZCRff/01/v7+D+0hfJwcOXIk6by0tBIfH5/g8QsUKEChQoWSdKkLkcSox0kkiUqUKMGoUaMYOnQop0+fpmXLluTMmZNTp06xdOlS3njjDQYNGpSifVeqVInOnTvz9ddfc+vWLerVq8euXbuYM2cOLVu2TJfhGM8//zwuLi40a9aMN998k4iICL755hsKFCjAxYsXU7TPd999lyVLltCmTRu6detG1apVuXHjBj///DMzZsygUqVKdOrUiUWLFtGzZ082bNhA7dq1iY+P5/DhwyxatMh2vaqHadeuHQcOHOCTTz5h7969dOjQgWLFinH9+nVWrVrFunXrbNc9eeONN/jqq6/o0qULe/bswd/fnyVLlrB161YmTZqU5EkmkqpUqVJ0796dP/74Ax8fH2bOnMnly5ftvgi+9NJLjBw5kq5du1KrVi0OHDjAvHnznmgM/vr16+nbty9t2rShVKlSxMXF8d133+Ho6Gg7/yO9PnPNmjWjQYMGfPDBB5w+fZpKlSqxZs0ali9fzoABAyhRokSqPE5ShYSEMGXKFDZs2PDI3o9Tp07RvHlzXnjhBbZv387333/Pq6++SqVKlYDUOR60bNmS6tWr884773D8+HHKlCnDzz//bLsmWFL+Yl+mTBlKlCjBoEGDOH/+PF5eXvz4448PPU/Hzc2NVatW0blzZ5555hl+++03Vq5cyfvvv//Q8wxTQ4sWLRJM+f6g5ByDqlatyvTp0xk1ahSBgYEUKFCAhg0bJumYk15Gjx5NixYtqF27Nl27duXmzZtMmzaNChUq2IWpxMybN4+8efPaLlfxoObNm/PNN9+wcuVKXn755QT3+/j40L9/f8aPH2/7HO/fv5/ffvuNfPny2X22hgwZwg8//ECTJk146623yJMnD3PmzOHUqVP8+OOPSRrKmpiqVauycOFCBg4cyNNPP42npyfNmjVL0b5S4vbt2xQpUoTWrVtTqVIlPD09Wbt2LX/88Qfjx49Ptzoka1FwEkmGIUOGUKpUKSZOnMiIESMA6zk4zz//PM2bN3+ifX/77bcUL16c2bNns3TpUnx9fRk6dCjDhg1LjdIfq3Tp0ixZsoQPP/yQQYMG4evrS69evcifP3+CGfmSytPTk82bNzNs2DCWLl3KnDlzKFCgAI0aNbKdZ+Pg4MCyZcuYOHEic+fOZenSpXh4eFC8eHH69++fpGFQo0aNomHDhkyZMoXp06dz48YNcufOTY0aNVi+fLntvXF3dyc0NJQhQ4YwZ84cwsPDKV26NLNmzUpwQcjUULJkSaZOncq7777LkSNHCAgIYOHChXYzIr7//vtERkYyf/58Fi5cSJUqVVi5ciVDhgxJ8eNWqlSJ4OBgfvnlF86fP4+HhweVKlXit99+o0aNGrb10uMz5+DgwM8//8xHH33EwoULmTVrFv7+/nz22We2GfzS073rTh06dMh2AdnELFy4kI8++oghQ4bg5ORE3759+eyzz+zWedLjgaOjIytXrqR///7MmTMHBwcHWrVqxbBhw6hduzZubm6P3YezszO//PKL7Rw2Nzc3WrVqRd++fRMNCY6OjqxatYpevXrx7rvvkjNnToYNG2Z3PTejJOcY9NFHH3HmzBnGjRvH7du3qVevHg0bNkzSMSe9NGvWjB9++IHhw4czZMgQSpYsyezZs5kzZw4HDx586HZXrlxh7dq1dOjQ4aHn8TRq1AgPDw++//77RIMTwNixY/Hw8OCbb75h7dq11KxZkzVr1vDss8/afbZ8fHzYtm0b7733HlOnTiUqKoqKFSvyyy+/PFHPc+/evdm3bx+zZs1i4sSJFCtWLF2Dk4eHB71792bNmjW2WTADAwP58ssvE52dUiQpTJbMdGaviEgm4e/vT4UKFWzDBCXjeOqpp8iTJw/r1q1LcN/w4cMZMWIEV69efeJzzFJq2bJltGrVii1btlC7du1U22+XLl1YsmTJY3s7JG1VrlyZ/PnzP/Q8tLR069YtcufOzahRo2wXDxaRpNM5TiIikm3s3r2bffv22U2kYKS7d+/aLcfHxzN16lS8vLyoUqWKQVVJaoiNjU1wrmFoaCj79++nfv36af74D3624P/PaUyPxxfJijRUT0REsry///6bPXv2MH78eAoWLEi7du2MLgmwXpD07t271KxZk+joaH766Se2bdvG6NGjU23aZzHG+fPnady4Ma+99hqFChXi8OHDzJgxA19fX3r27Jnmj79w4UJmz57Niy++iKenJ1u2bOGHH37g+eefT9WeTJHsRMFJRESyvCVLljBy5EhKly7NDz/8kKTzh9JDw4YNGT9+PCtWrCAqKorAwECmTp1K3759jS5NnlDu3LmpWrUq3377LVevXiVHjhw0bdqUTz/9NNGLW6e2ihUr4uTkxLhx4wgPD7dNGDFq1Kg0f2yRrErnOImIiIiIiDyGznESERERERF5DAUnERERERGRx8h25ziZzWYuXLhAzpw5k3RxQRERERERyZosFgu3b9+mUKFCj73gc7YLThcuXMDPz8/oMkREREREJIM4d+7cYy+Une2CU86cOQHri+Pl5WVwNSIiIiIiYpTw8HD8/PxsGeFRsl1wujc8z8vLS8FJRERERESSdAqPJocQERERERF5DAUnERERERGRx1BwEhEREREReYxsd45TUlgsFuLi4oiPjze6FJHHcnR0xMnJSdPri4iIiKQhBacHxMTEcPHiRe7cuWN0KSJJ5uHhQcGCBXFxcTG6FBEREZEsScHpPmazmVOnTuHo6EihQoVwcXHRX/ElQ7NYLMTExHD16lVOnTpFyZIlH3vxNhERERFJPgWn+8TExGA2m/Hz88PDw8PockSSxN3dHWdnZ86cOUNMTAxubm5GlyQiIiKS5ehP04nQX+wls9FnVkRERCRt6duWiIiIiIjIYyg4iYiIiIiIPIaCk6Sp4cOHU7lyZaPLeGKhoaGYTCZu3br10HWyynMVERERkYQUnLKQS5cu0b9/fwIDA3Fzc8PHx4fatWszffr0TD29ur+/PyaTiQULFiS4r3z58phMJmbPnp3k/c2ePZtcuXKlXoH/M2jQINatW5fq+xURERER42lWvSzi5MmT1K5dm1y5cjF69GiCgoJwdXXlwIEDfP311xQuXJjmzZsnum1sbCzOzs7pXHHy+Pn5MWvWLNq3b29r27FjB5cuXSJHjhwGVvb/PD098fT0NLoMEREREUkD6nF6DIvFwp2YOENuFoslyXX27t0bJycndu/eTdu2bSlbtizFixenRYsWrFy5kmbNmtnWNZlMTJ8+nebNm5MjRw4++eQTAJYvX06VKlVwc3OjePHijBgxgri4ONt2t27d4vXXXyd//vx4eXnRsGFD9u/fb1fHp59+io+PDzlz5qR79+5ERUXZ7tu0aRPOzs5cunTJbpsBAwZQp06dRz6/jh07snHjRs6dO2drmzlzJh07dsTJyT7/T5gwgaCgIHLkyIGfnx+9e/cmIiICsA6569q1K2FhYZhMJkwmE8OHDwcgOjqa9957Dz8/P1xdXQkMDOS///2v3b737NlDtWrV8PDwoFatWhw5csR234ND9bp06ULLli35/PPPKViwIHnz5qVPnz7Exsba1rl48SJNmzbF3d2dgIAA5s+fj7+/P5MmTXrk6yEiIiIi6Us9To9xNzaech+tNuSx/xkZjIfL49+i69evs2bNGkaPHv3Q3pcHL+Q7fPhwPv30UyZNmoSTkxObN28mJCSEKVOmUKdOHU6cOMEbb7wBwLBhwwBo06YN7u7u/Pbbb3h7e/PVV1/RqFEjjh49Sp48eVi0aBHDhw/niy++4Nlnn+W7775jypQpFC9eHIC6detSvHhxvvvuO959913A2ts1b948xo0b98jn6OPjQ3BwMHPmzOHDDz/kzp07LFy4kI0bNzJ37ly7dR0cHJgyZQoBAQGcPHmS3r17M3jwYL788ktq1arFpEmT+Oijj2yh514vUUhICNu3b2fKlClUqlSJU6dOce3aNbt9f/DBB4wfP578+fPTs2dPunXrxtatWx9a94YNGyhYsCAbNmzg+PHjtGvXjsqVK9OjRw/bY167do3Q0FCcnZ0ZOHAgV65ceeRrISIiIiLpTz1OWcDx48exWCyULl3arj1fvny24WPvvfee3X2vvvoqXbt2pXjx4hQtWpQRI0YwZMgQOnfuTPHixXnuuef4+OOP+eqrrwDYsmULu3btYvHixVSrVo2SJUvy+eefkytXLpYsWQLApEmT6N69O927d6d06dKMGjWKcuXK2T1u9+7dmTVrlm35l19+ISoqirZt2z72eXbr1o3Zs2djsVhYsmQJJUqUSHQyhgEDBtCgQQP8/f1p2LAho0aNYtGiRQC4uLjg7e2NyWTC19cXX19fPD09OXr0KIsWLWLmzJm0atWK4sWL06hRI9q1a2e3708++YR69epRrlw5hgwZwrZt2+x61R6UO3dupk2bRpkyZXjppZdo2rSp7Tyow4cPs3btWr755hueeeYZqlSpwrfffsvdu3cf+1qIiIiISPpSj9NjuDs78s/IYMMe+0ns2rULs9lMx44diY6OtruvWrVqdsv79+9n69attmF7APHx8URFRXHnzh32799PREQEefPmtdvu7t27nDhxAoBDhw7Rs2dPu/tr1qzJhg0bbMtdunThww8/ZMeOHdSoUYPZs2fTtm3bJJ2n1LRpU9588002bdrEzJkz6datW6LrrV27ljFjxnD48GHCw8OJi4uzPQ8PD49Et9m3bx+Ojo7U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SEIE/6amtWvX4sUXX0RZWRlSUlLw+uuvY/To0R77b926Fc888wzOnTuHpKQkrF69GlOmTPHrvSorKxEZGYmrV6/CbDYH6hSIiIiIiKidaUw20LXSmDx6++238cQTT2DlypU4cuQIUlJSMHHiRFRUVGj2//zzzzFnzhzcf//9OHr0KDIzM5GZmYmTJ0+28siJiIiIiOhGEfQrTmPGjMGoUaPw5z//GQAgyzJ69eqF3/72t1i6dKlb/1mzZqGmpgY7duxQ2m699VYMHToU69evd+tfV1eHuro65XVlZSV69erFK05ERERERDe4dnPFyWKxoKCgAOnp6UqbTqdDeno68vLyNPfJy8tT9QeAiRMneuy/atUqREZGKo9evXoF7gSIiIiIiOiGENTgdPHiRdjtdsTGxqraY2NjUVZWprlPWVlZo/ovW7YMV69eVR7nz58PzOCJiIiIiOiG0eG/Vc9kMsFkMgV7GERERERE1I4F9YpTdHQ09Ho9ysvLVe3l5eWIi4vT3CcuLq5R/YmIiIiIiJorqMHJaDRixIgR2LNnj9ImyzL27NmD1NRUzX1SU1NV/QFg9+7dHvsTERERERE1V9Bv1XviiSeQlZWFkSNHYvTo0fjTn/6EmpoazJs3DwDw61//GjfddBNWrVoFAHjssccwfvx4/PGPf8TUqVOxZcsW5Ofn469//WswT4OIiIiIiDqwoAenWbNm4fvvv8eKFStQVlaGoUOHYteuXcoXQJSUlECna7gwNnbsWPz73//G7373Ozz99NNISkrC9u3bMXjw4GCdAhERERERdXBB/z1Ora0x39VOREREREQdV7v5PU5ERERERETtAYMTERERERGRDwxOREREREREPjA4ERERERER+cDgRERERERE5AODExERERERkQ8MTkRERERERD4wOBEREREREflgCPYAWpvj9/1WVlYGeSRERERERBRMjkzgyAje3HDBqaqqCgDQq1evII+EiIiIiIjagqqqKkRGRnrtIwl/4lUHIssyLly4gIiICEiSFOzhoLKyEr169cL58+dhNpuDPZwOh/VtWaxvy2J9Wxbr27JY35bF+rYs1rdltaX6CiFQVVWFHj16QKfz/lNMN9wVJ51Oh549ewZ7GG7MZnPQJ05Hxvq2LNa3ZbG+LYv1bVmsb8tifVsW69uy2kp9fV1pcuCXQxAREREREfnA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XY2zZ88iPj6+4/z7G+xvp+iIqqqqxNGjR8XRo0cFAPHyyy+Lo0ePim+//VYIIUR2draIiooS77//vjh+/LiYPn26SExMFD/++KNyjEmTJolhw4aJQ4cOif3794ukpCQxZ86cYJ1Sm+KtvlVVVeLJJ58UeXl5ori4WPznP/8Rw4cPF0lJSaK2tlY5Buvr2cMPPywiIyNFTk6OKC0tVR7Xrl1T+jz00EPi5ptvFnv37hX5+fkiNTVVpKamKtttNpsYPHiwyMjIEIWFhWLXrl2ie/fuYtmyZcE4pTbFV33PnDkjfv/734v8/HxRXFws3n//fdG7d28xbtw45Risr2dLly4Vubm5ori4WBw/flwsXbpUSJIkPvnkEyEE525zeasv527guX7DG+dv4DnXmHO4eRYvXixycnJEcXGxOHDggEhPTxfR0dGioqJCCNEx5i+DUwvYt2+fAOD2yMrKEkLUfyX5M888I2JjY4XJZBJpaWmiqKhIdYxLly6JOXPmiPDwcGE2m8W8efNEVVVVEM6m7fFW32vXromMjAzRvXt3ERISIhISEsT8+fNVX20pBOvrjVZtAYiNGzcqfX788UfxyCOPiC5duojOnTuLX/ziF6K0tFR1nHPnzonJkyeL0NBQER0dLRYvXiysVmsrn03b46u+JSUlYty4caJr167CZDKJvn37iiVLloirV6+qjsP6arvvvvtEQkKCMBqNonv37iItLU0JTUJw7jaXt/py7gaea3Di/A085xpzDjfPrFmzRHx8vDAajeKmm24Ss2bNEmfOnFG2d4T5KwkhROtd3yIiIiIiImp/+DNOREREREREPjA4ERERERER+cDgRERERERE5AODExERERERkQ8MTkRERERERD4wOBEREREREfnA4EREREREROQDgxMREREREZEPDE5ERERNdO7cOUiShMLCwoAeNycnB5Ik4cqVKwE9LhERNR2DExERabr33nuRmZnp1s5FffMdO3YMP//5zxETE4NOnTrhlltuwaxZs1BRUQEAGDt2LEpLSxEZGRnkkRIRkQODExERtUsWi6VJ+9ntdsiyHODR+O/7779HWloaunbtio8//hinT5/Gxo0b0aNHD9TU1AAAjEYj4uLiIElS0MZJRERqDE5ERNRkNTU1MJvNePfdd1Xt27dvR1hYGKqqqpTb2bZs2YKxY8eiU6dOGDx4MHJzc1X7nDx5EpMnT0Z4eDhiY2Nxzz334OLFi8r22267DQsXLsSiRYsQHR2NiRMnAgA++OADJCUloVOnTpgwYQI2b96suiK2adMmREVF4YMPPsDAgQNhMplQUlKCw4cP44477kB0dDQiIyMxfvx4HDlyRDUmSZKwbt06TJ48GaGhoejdu7fbuQLAf//7X0yYMAGdO3dGSkoK8vLyPNbswIEDuHr1KjZs2IBhw4YhMTEREyZMwCuvvILExEQA7lf1brvtNkiS5PY4d+4cAODKlSt44IEH0L17d5jNZtx+++04duyY779AIiLyG4MTERE1WVhYGGbPno2NGzeq2jdu3Ig777wTERERStuSJUuwePFiHD16FKmpqZg2bRouXboEoH7hf/vtt2PYsGHIz8/Hrl27UF5ejl/+8peq427evBlGoxEHDhzA+vXrUVxcjDvvvBOZmZk4duwYHnzwQSxfvtxtnNeuXcPq1auxYcMGnDp1CjExMaiqqkJWVhb279+PgwcPIikpCVOmTEFVVZVq32eeeQYzZ87EsWPHMHfuXMyePRunT59W9Vm+fDmefPJJFBYWol+/fpgzZw5sNptmzeLi4mCz2bBt2zYIIfyq83vvvYfS0lLlMWPGDPTv3x+xsbEAgLvuugsVFRXYuXMnCgoKMHz4cKSlpeHy5ct+HZ+IiPwgiIiINGRlZQm9Xi/CwsJUj06dOgkA4ocffhBCCHHo0CGh1+vFhQsXhBBClJeXC4PBIHJycoQQQhQXFwsAIjs7Wzm21WoVPXv2FKtXrxZCCPH888+LjIwM1fufP39eABBFRUVCCCHGjx8vhg0bpurz1FNPicGDB6vali9frhrfxo0bBQBRWFjo9XztdruIiIgQH374odIGQDz00EOqfmPGjBEPP/yw6tw2bNigbD916pQAIE6fPu3xvZ5++mlhMBhE165dxaRJk8SaNWtEWVmZsn3fvn2qc3D28ssvi6ioKKUun332mTCbzaK2tlbVr0+fPuKNN97wes5EROQ/XnEiIiKPJkyYgMLCQtVjw4YNqj6jR4/GoEGDsHnzZgDAP//5TyQkJGDcuHGqfqmpqcpzg8GAkSNHKldujh07hn379iE8PFx5DBgwAABw9uxZZb8RI0aojllUVIRRo0a5jceV0WhEcnKyqq28vBzz589HUlISIiMjYTabUV1djZKSEo/jdrx2veLkfOz4+HgAUL7oQcsLL7yAsrIyrF+/HoMGDcL69esxYMAAnDhxwuM+ALBz504sXboUb7/9Nvr16wegvnbV1dXo1q2bqn7FxcWq2hERUfMYgj0AIiJqu8LCwtC3b19V23fffefW74EHHsDatWuxdOlSbNy4EfPmzWvUFxtUV1dj2rRpWL16tds2RxBxjKcpQkND3caTlZWFS5cu4dVXX0VCQgJMJhNSU1Ob9KUTISEhynPH+/j6Aopu3brhrrvuwl133YU//OEPGDZsGF566SUlgLr68ssvMXv2bGRnZyMjI0Npr66uRnx8PHJyctz2iYqKavS5EBGRNl5xIiKiZrv77rvx7bff4rXXXsOXX36JrKwstz4HDx5UnttsNhQUFOAnP/kJAGD48OE4deoUbrnlFvTt21f18BaW+vfvj/z8fFXb4cOH/RrzgQMH8Oijj2LKlCkYNGgQTCaT6ssotMbteO0Yd6AYjUb06dNH+VY9VxcvXsS0adMwc+ZMPP7446ptw4cPR1lZGQwGg1vtoqOjAzpOIqIbGYMTERE1W5cuXTBjxgwsWbIEGRkZ6Nmzp1uftWvXYtu2bfjqq6+wYMEC/PDDD7jvvvsAAAsWLMDly5cxZ84cHD58GGfPnsXHH3+MefPmwW63e3zfBx98EF999RWeeuopfP3113jnnXewadMmAPB5xSspKQlvvvkmTp8+jUOHDmHu3LkIDQ1167d161b8/e9/x9dff42VK1fiiy++wMKFCxtRHbUdO3bg7rvvxo4dO/D111+jqKgIL730Ej766CNMnz5dc5+ZM2eic+fOePbZZ1FWVqY87HY70tPTkZqaiszMTHzyySc4d+4cPv/8cyxfvtwtVBIRUdMxOBERUUDcf//9sFgsShhylZ2djezsbKSkpGD//v344IMPlCsiPXr0wIEDB2C325GRkYEhQ4Zg0aJFiIqKgk7n+T9ViYmJePfdd/Hee+8hOTkZ69atU75Vz2QyeR3v3/72N/zwww8YPnw47rnnHjz66KOIiYlx6/fcc89hy5YtSE5Oxj/+8Q+89dZbGDhwoL9lcTNw4EB07twZixcvxtChQ3HrrbfinXfewYYNG3DPPfdo7vPpp5/i5MmTSEhIQHx8vPI4f/48JEnCRx99hHHjxmHevHno168fZs+ejW+//Vb51j0iImo+SQg/vwuViIjIizfffBOPP/44Lly4AKPRqLSfO3cOiYmJOHr0KIYOHdri43jhhRewfv16nD9/vtnHkiQJ27ZtQ2ZmZvMHRkRE7Rq/HIKIiJrl2rVrKC0tRXZ2Nh588EFVaGoNf/nLXzBq1Ch069YNBw4cwIsvvtisW+mIiIi08FY9IiJqljVr1mDAgAGIi4vDsmXLWv39v/nmG0yfPh0DBw7E888/j8WLF+PZZ59t9XEQEVHHxlv1iIiIiIiIfOAVJyIiIiIiIh8YnIiIiIiIiHxgcCIiIiIiIvKBwYmIiIiIiMgHBiciIiIiIiIfGJyIiIiIiIh8YHAiIiIiIiLygcGJiIiIiIjIh/8HbWpIxIqpywcAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def generate_random_hypergraph(n, d, m):\n", + " edges = {f'e{i}': random.sample(range(1, n+1), d) for i in range(m)}\n", + " return Hypergraph(edges)\n", + "\n", + "# Generate random hypergraphs of increasing size\n", + "sizes = [100, 200, 300, 400, 500]\n", + "greedy_times = []\n", + "iterated_times = []\n", + "hedcs_times = []\n", + "\n", + "for size in sizes:\n", + " hypergraph = generate_random_hypergraph(size, 3, size)\n", + " \n", + " start_time = time.time()\n", + " greedy_matching(hypergraph, k)\n", + " greedy_times.append(time.time() - start_time)\n", + " \n", + " start_time = time.time()\n", + " iterated_sampling(hypergraph, s, max_iterations = 500)\n", + " iterated_times.append(time.time() - start_time)\n", + " \n", + " start_time = time.time()\n", + " HEDCS_matching(hypergraph, s)\n", + " hedcs_times.append(time.time() - start_time)\n", + "\n", + "# Plot the results\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(sizes, greedy_times, label='Greedy Matching')\n", + "plt.plot(sizes, iterated_times, label='Iterated Sampling')\n", + "plt.plot(sizes, hedcs_times, label='HEDCS Matching')\n", + "plt.xlabel('Hypergraph Size')\n", + "plt.ylabel('Time (seconds)')\n", + "plt.title('Performance Comparison of Hypergraph Matching Algorithms')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "In this tutorial, we demonstrated the implementation and usage of several hypergraph matching algorithms. We also compared their performance on random hypergraphs of increasing size.\n", + "\n", + "For more details, please refer to our publication: [Distributed Algorithms for Matching in Hypergraphs](https://arxiv.org/abs/2009.09605v1)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}