diff --git a/.cz.toml b/.cz.toml index ee9e1967..45c84afd 100644 --- a/.cz.toml +++ b/.cz.toml @@ -1,6 +1,6 @@ [tool.commitizen] name = "cz_conventional_commits" -version = "2.3.7" +version = "2.3.8" version_provider = "poetry" version_files = [ "pyproject.toml", 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/docs/source/conf.py b/docs/source/conf.py index fa5f7849..52050268 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ import os -__version__ = "2.3.7" +__version__ = "2.3.8" # If extensions (or modules to document with autodoc) are in another directory, diff --git a/docs/source/contributions.rst b/docs/source/contributions.rst new file mode 100644 index 00000000..8a50041b --- /dev/null +++ b/docs/source/contributions.rst @@ -0,0 +1,56 @@ +HyperNetX Contributor Guidelines +**************************************************** + +We welcome contributions to HyperNetX! +This document outlines the process for contributing to various aspects of the codebase. +We currently only provide guidelines for new modules, but we are not opposed to other forms of contribution to the HyperNetX library. + +Contributing New Modules +============================ + +We happily accept the contribution of new modules or methods to the HyperNetX library. We will do our best to keep modules functioning with new release but may ask contributors to update code when possible. +Contributions can be slow, but the closer the pull request is to our guidelines the faster this process will be. +The required new files for any new module are listed below and an example file structure of the additional files is shown in the figure below. + +.. image:: ./images/module_addition_file_structure.png + :width: 330px + :align: right + +* Python file: Create a new Python file named **.py** under the folder **hypernetx/hypernetx/algorithms/.**. This file will contain the core functionalities of your module. All methods need to have docstrings in the new module. + +* Jupyter Notebook: Create a Jupyter notebook under in the folder **hypernetx/tutorials/advanced/.** that demonstrates usage examples of your module. This notebook should be named **Advanced - .ipynb**. Please look at the current advanced module number and choose an appropriate number. + +* Test file: Write unit tests for your module in a file named **test_.py** under the tests folder located at **hypernetx/tests/algorithms/.**. These tests should ensure the correctness and functionality of your code. + +* Documentation: Write an rst file named **.rst** under the algorithms documentation folder located at **hypernetx/docs/source/algorithms/.**. This documentations should focus on the mathematics or theory behind your module with citations to relevant papers and documents. Additionally it should provide code snippets demonstrating usage that parallel the tutorial. + +Step-by-Step Process +~~~~~~~~~~~~~~~~~~~~ + +#. Branch Creation: Create a new branch from the main development branch for your contribution. This allows you to isolate your changes and work independently. Use a descriptive branch name that reflects the module you're adding (e.g., add_). + +#. Code Implementation: Implement the functionalities of your module in a new **.py** file located in **hypernetx/hypernetx/algorithms/.**. Please validate that your code dependencies are not in conflict with the core HNX dependencies. Any additional dependencies should be documented thoroughly including in the notebook creation step. + +#. Documentation: Write docstrings for your code to explain the purpose and usage of functions and classes. Additionally provide an overview description of the module in the python file. For an example of the correct docstring format please see the module **hypernetx/hypernetx/algorithms/s_centrality_measures.py**. + +#. Jupyter Notebook Creation: Create a Jupyter notebook named **Advanced - .ipynb** under advanced tutorials folder **hypernetx/tutorials/advanced/.**. This notebook should showcase how to use your module and demonstrate its capabilities with thorough documentation. Additionally, in the notebook and any other documentation please provide clear documentation on any new dependencies outside of core HNX that are required. + +#. Testing: Write unit tests in the test_.py file to ensure your module functions as expected. This should be located in the algorithm tests folder. In the top hypernetx directory you can use the makefile and the command ``make test`` to validate everything is passing. Please see other tests and follow a similar format. + +# Read the Docs: Include your rst file in the algorithms folder of the source docs overviewing the theory/mathematics of the new module with example code. See other rst files as examples of formatting. + +#. __init__.py Update: Update the __init__.py file in the **hypernetx/hypernetx/algorithms/** folder to import your new module. Please follow the style of importing used by the other modules. + +#. Commit and Push: Commit your changes with clear and concise commit messages describing each modification. Push your commits to your branch on the remote repository. + +#. Pull Request: Create a pull request from your branch to the main development branch. This will initiate a code review process. + +Additional Notes +~~~~~~~~~~~~~~~~~~~~ + +* Make sure your code adheres to PEP 8 style guidelines for Python code. +* Please add comments to your code to explain complex logic or non-obvious functionalities. +* During the review process, address any feedback or suggestions from reviewers promptly. +* Any dependencies in core HNX should not be changed for new modules. Also please list any additionally dependencies thoroughly. + +By following these guidelines, you can ensure a smooth and efficient contribution process for adding new modules to HyperNetX. We appreciate your contributions to the project! \ No newline at end of file diff --git a/docs/source/images/module_addition_file_structure.png b/docs/source/images/module_addition_file_structure.png new file mode 100644 index 00000000..6308decb Binary files /dev/null and b/docs/source/images/module_addition_file_structure.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst index df2b8110..b0010eb8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -61,6 +61,7 @@ Contents Visualization Widget Algorithms: Modularity and Clustering Publications + Contributors Guide license diff --git a/hypernetx/__init__.py b/hypernetx/__init__.py index 3acc7796..d198b9f7 100644 --- a/hypernetx/__init__.py +++ b/hypernetx/__init__.py @@ -11,4 +11,4 @@ from hypernetx.utils import * from hypernetx.utils.toys import * -__version__ = "2.3.7" +__version__ = "2.3.8" diff --git a/hypernetx/algorithms/__init__.py b/hypernetx/algorithms/__init__.py index 78b30c49..3dd7129d 100644 --- a/hypernetx/algorithms/__init__.py +++ b/hypernetx/algorithms/__init__.py @@ -19,6 +19,13 @@ hypergraph_homology_basis, interpret, ) +from hypernetx.algorithms.matching_algorithms import ( + greedy_matching, + maximal_matching, + iterated_sampling, + HEDCS_matching, + approximation_matching_checking, +) from hypernetx.algorithms.s_centrality_measures import ( s_betweenness_centrality, s_harmonic_closeness_centrality, @@ -116,4 +123,10 @@ "two_section", "kumar", "last_step", + # matching_algorithms API's + "greedy_matching", + "maximal_matching", + "iterated_sampling", + "HEDCS_matching", + "approximation_matching_checking", ] diff --git a/hypernetx/algorithms/matching_algorithms.py b/hypernetx/algorithms/matching_algorithms.py new file mode 100644 index 00000000..ef13e8df --- /dev/null +++ b/hypernetx/algorithms/matching_algorithms.py @@ -0,0 +1,597 @@ +""" +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/pyproject.toml b/pyproject.toml index e50760f4..38627341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hypernetx" -version = "2.3.7" +version = "2.3.8" description = "HyperNetX is a Python library for the creation and study of hypergraphs." authors = ["Brenda Praggastis ", "Dustin Arendt ", "Sinan Aksoy ", "Emilie Purvine ", 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/Gxo0b0aNHD9TU1AAAjEYj4uLiIElS0MZJRERqDE5ERNRkNTU1MJvNePfdd1Xt27dvR1hYGKqqqpTb2bZs2YKxY8eiU6dOGDx4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", + "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 +}