From 4e8a9b3cf8d3d657f824e682b09a7863b1349a8b Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 17 Apr 2023 11:10:38 +0200 Subject: [PATCH 1/6] feat: added complete hypergraph --- xgi/generators/classic.py | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/xgi/generators/classic.py b/xgi/generators/classic.py index d7b4245d2..d1072be53 100644 --- a/xgi/generators/classic.py +++ b/xgi/generators/classic.py @@ -154,3 +154,55 @@ def trivial_hypergraph(n=1, create_using=None, default=None): H.add_nodes_from(nodes) return H + + +def complete_hypergraph(N, order=None, max_order=None, include_singletons=False): + """ + Generate a complete hypergraph, i.e. one that contains all possible hyperdges + at a given `order` or up to a `max_order`. + + Parameters + ---------- + + N : int + Number of nodes + order : int or None + If not None (default), specifies the single order for which to generate hyperedges + max_order : int or None + If not None (default), specifies the maximum order for which to generate hyperedges + include_singletons : bool + Whether to include singleton edges (default: False). This argument is discarded + if max_order is None. + + Return + ------ + Hypergraph object + A complete hypergraph with `N` nodes + + Note + ---- + Only one of `order` and `max_order` can be specified by and int (not None). + Additionally, at least one of either must be specified. + """ + + if bool(order) == bool(max_order): + raise ValueError("One (and one only) among order and max_order must be specified (not None)") + + H = xgi.Hypergraph() + + nodes = range(N) + H.add_nodes_from(nodes) + + if order is not None: + edges = combinations(nodes, order + 1) + elif max_order is not None: + start = 1 if include_singletons else 2 + end = max_order + 1 + + s = list(nodes) + edges = chain.from_iterable(combinations(s, r) for r in range(start, end + 1)) + + H.add_edges_from(edges) + + return H + From 833756f4243e43a58008354b25108dfc4263c900 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 17 Apr 2023 11:33:24 +0200 Subject: [PATCH 2/6] tests: added corresponding --- tests/generators/test_classic.py | 54 +++++++++++++++++++++++++++++++- xgi/generators/classic.py | 6 ++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/tests/generators/test_classic.py b/tests/generators/test_classic.py index db651f17d..114b6ead6 100644 --- a/tests/generators/test_classic.py +++ b/tests/generators/test_classic.py @@ -1,3 +1,6 @@ +from math import comb +import pytest + import xgi @@ -18,4 +21,53 @@ def test_trivial_hypergraph(): assert (H.num_nodes, H.num_edges) == (1, 0) H = xgi.trivial_hypergraph(n=2) - assert (H.num_nodes, H.num_edges) == (2, 0) \ No newline at end of file + assert (H.num_nodes, H.num_edges) == (2, 0) + +def test_complete_hypergraph(): + + N = 4 + + # single order + H1 = xgi.complete_hypergraph(N=N, order=1) + H2 = xgi.complete_hypergraph(N=N, order=2) + H3 = xgi.complete_hypergraph(N=N, order=3) + H03 = xgi.complete_hypergraph(N=N, order=3, include_singletons=True) + + assert H3._edge == H03._edge + + assert H1.edges.members() == [{0, 1}, {0, 2}, {0, 3}, {1, 2}, {1, 3}, {2, 3}] + assert H2.edges.members() == [{0, 1, 2}, {0, 1, 3}, {0, 2, 3}, {1, 2, 3}] + assert H3.edges.members() == [{0, 1, 2, 3}] + + assert xgi.unique_edge_sizes(H1) == [2] + assert xgi.unique_edge_sizes(H2) == [3] + assert xgi.unique_edge_sizes(H3) == [4] + + assert H1.num_edges == comb(N, 2) + assert H2.num_edges == comb(N, 3) + assert H3.num_edges == comb(N, 4) + + # max_order + H1 = xgi.complete_hypergraph(N=N, max_order=1) + H2 = xgi.complete_hypergraph(N=N, max_order=2) + H3 = xgi.complete_hypergraph(N=N, max_order=3) + H03 = xgi.complete_hypergraph(N=N, max_order=3, include_singletons=True) + + assert xgi.unique_edge_sizes(H1) == [2] + assert xgi.unique_edge_sizes(H2) == [2, 3] + assert xgi.unique_edge_sizes(H3) == [2, 3, 4] + assert xgi.unique_edge_sizes(H03) == [1, 2, 3, 4] + + assert H1.num_edges == comb(N, 2) + assert H2.num_edges == comb(N, 2) + comb(N, 3) + assert H3.num_edges == comb(N, 2) + comb(N, 3) + comb(N, 4) + assert H03.num_edges == comb(N, 2) + comb(N, 3) + comb(N, 4) + N + + # errors + with pytest.raises(ValueError): + H1 = xgi.complete_hypergraph(N=N, order=1, max_order=2) + + with pytest.raises(ValueError): + H1 = xgi.complete_hypergraph(N=N, order=None, max_order=None) + + diff --git a/xgi/generators/classic.py b/xgi/generators/classic.py index d1072be53..27eb71644 100644 --- a/xgi/generators/classic.py +++ b/xgi/generators/classic.py @@ -5,10 +5,13 @@ """ +from itertools import chain, combinations + __all__ = [ "empty_hypergraph", "empty_simplicial_complex", "trivial_hypergraph", + "complete_hypergraph" ] @@ -184,6 +187,9 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) Only one of `order` and `max_order` can be specified by and int (not None). Additionally, at least one of either must be specified. """ + # this import needs to happen when the function runs, not when the module is first + # imported, to avoid circular imports + import xgi if bool(order) == bool(max_order): raise ValueError("One (and one only) among order and max_order must be specified (not None)") From e6d5c1f10533281b8912b534bfae6575ed365a54 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 17 Apr 2023 11:34:34 +0200 Subject: [PATCH 3/6] docs: added new function. style: black isort --- .../api/generators/xgi.generators.classic.rst | 3 ++- tests/generators/test_classic.py | 7 +++--- xgi/generators/classic.py | 23 ++++++++++--------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/source/api/generators/xgi.generators.classic.rst b/docs/source/api/generators/xgi.generators.classic.rst index 96d832d7c..cc88db921 100644 --- a/docs/source/api/generators/xgi.generators.classic.rst +++ b/docs/source/api/generators/xgi.generators.classic.rst @@ -9,4 +9,5 @@ .. autofunction:: empty_hypergraph .. autofunction:: empty_simplicial_complex - .. autofunction:: trivial_hypergraph \ No newline at end of file + .. autofunction:: trivial_hypergraph + .. autofunction:: complete_hypergraph \ No newline at end of file diff --git a/tests/generators/test_classic.py b/tests/generators/test_classic.py index 114b6ead6..4ca56c658 100644 --- a/tests/generators/test_classic.py +++ b/tests/generators/test_classic.py @@ -1,4 +1,5 @@ from math import comb + import pytest import xgi @@ -13,6 +14,7 @@ def test_empty_hypergraph(): SC = xgi.empty_simplicial_complex() assert (SC.num_nodes, SC.num_edges) == (0, 0) + def test_trivial_hypergraph(): H = xgi.trivial_hypergraph() assert (H.num_nodes, H.num_edges) == (1, 0) @@ -23,6 +25,7 @@ def test_trivial_hypergraph(): H = xgi.trivial_hypergraph(n=2) assert (H.num_nodes, H.num_edges) == (2, 0) + def test_complete_hypergraph(): N = 4 @@ -38,7 +41,7 @@ def test_complete_hypergraph(): assert H1.edges.members() == [{0, 1}, {0, 2}, {0, 3}, {1, 2}, {1, 3}, {2, 3}] assert H2.edges.members() == [{0, 1, 2}, {0, 1, 3}, {0, 2, 3}, {1, 2, 3}] assert H3.edges.members() == [{0, 1, 2, 3}] - + assert xgi.unique_edge_sizes(H1) == [2] assert xgi.unique_edge_sizes(H2) == [3] assert xgi.unique_edge_sizes(H3) == [4] @@ -69,5 +72,3 @@ def test_complete_hypergraph(): with pytest.raises(ValueError): H1 = xgi.complete_hypergraph(N=N, order=None, max_order=None) - - diff --git a/xgi/generators/classic.py b/xgi/generators/classic.py index 27eb71644..23317cc2e 100644 --- a/xgi/generators/classic.py +++ b/xgi/generators/classic.py @@ -11,7 +11,7 @@ "empty_hypergraph", "empty_simplicial_complex", "trivial_hypergraph", - "complete_hypergraph" + "complete_hypergraph", ] @@ -163,10 +163,10 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) """ Generate a complete hypergraph, i.e. one that contains all possible hyperdges at a given `order` or up to a `max_order`. - + Parameters ---------- - + N : int Number of nodes order : int or None @@ -174,14 +174,14 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) max_order : int or None If not None (default), specifies the maximum order for which to generate hyperedges include_singletons : bool - Whether to include singleton edges (default: False). This argument is discarded + Whether to include singleton edges (default: False). This argument is discarded if max_order is None. - + Return ------ Hypergraph object A complete hypergraph with `N` nodes - + Note ---- Only one of `order` and `max_order` can be specified by and int (not None). @@ -190,10 +190,12 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) # this import needs to happen when the function runs, not when the module is first # imported, to avoid circular imports import xgi - + if bool(order) == bool(max_order): - raise ValueError("One (and one only) among order and max_order must be specified (not None)") - + raise ValueError( + "One (and one only) among order and max_order must be specified (not None)" + ) + H = xgi.Hypergraph() nodes = range(N) @@ -209,6 +211,5 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) edges = chain.from_iterable(combinations(s, r) for r in range(start, end + 1)) H.add_edges_from(edges) - - return H + return H From f740cba642442991e2361ae49524d4c4435af522 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 17 Apr 2023 16:46:36 +0200 Subject: [PATCH 4/6] review comments --- xgi/generators/classic.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/xgi/generators/classic.py b/xgi/generators/classic.py index 23317cc2e..fdc341935 100644 --- a/xgi/generators/classic.py +++ b/xgi/generators/classic.py @@ -182,18 +182,28 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) Hypergraph object A complete hypergraph with `N` nodes - Note + Notes ---- Only one of `order` and `max_order` can be specified by and int (not None). Additionally, at least one of either must be specified. + + The number of possible edges grows exponentially as $\approx 2^N$ for large `N` and + quickly becomes impractically long to compute, especially when using `max_order`. For + example, `N=100` and `max_order=5` already yields $8 10^7$ edges. Increasing `N=1000` + makes it $8 10^{12}$. `N=100` and with a larger `max_order=6` yields $8 10^8$ edgfes. + """ # this import needs to happen when the function runs, not when the module is first # imported, to avoid circular imports import xgi - if bool(order) == bool(max_order): + if (order is None) and (max_order is None): + raise ValueError( + "At least one among order and max_order must be specified (not None)" + ) + if (order is not None) and (max_order is not None): raise ValueError( - "One (and one only) among order and max_order must be specified (not None)" + "Both order and max_order cannot be specified (not None) at the same time." ) H = xgi.Hypergraph() From 9008454ee186c1ab000814c19e3c235011782f93 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 17 Apr 2023 17:01:47 +0200 Subject: [PATCH 5/6] review comments --- xgi/generators/classic.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/xgi/generators/classic.py b/xgi/generators/classic.py index fdc341935..6930871c1 100644 --- a/xgi/generators/classic.py +++ b/xgi/generators/classic.py @@ -187,10 +187,11 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) Only one of `order` and `max_order` can be specified by and int (not None). Additionally, at least one of either must be specified. - The number of possible edges grows exponentially as $\approx 2^N$ for large `N` and - quickly becomes impractically long to compute, especially when using `max_order`. For - example, `N=100` and `max_order=5` already yields $8 10^7$ edges. Increasing `N=1000` - makes it $8 10^{12}$. `N=100` and with a larger `max_order=6` yields $8 10^8$ edgfes. + The number of possible edges grows exponentially as :math:`\approx 2^N for large `N` and + quickly becomes impractically long to compute, especially when using `max_order`. For + example, `N=100` and `max_order=5` already yields :math:`8\times 10^7` edges. Increasing `N=1000` + makes it :math:`8\times 10^{12}`. `N=100` and with a larger `max_order=6` yields :math:`8\times 10^8` edges. + """ # this import needs to happen when the function runs, not when the module is first From 6ab8bce1440677e9bec958039615a0d32a111976 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Mon, 17 Apr 2023 17:56:41 +0200 Subject: [PATCH 6/6] fix docs maths --- xgi/generators/classic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xgi/generators/classic.py b/xgi/generators/classic.py index 6930871c1..9a37f14c0 100644 --- a/xgi/generators/classic.py +++ b/xgi/generators/classic.py @@ -187,10 +187,10 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) Only one of `order` and `max_order` can be specified by and int (not None). Additionally, at least one of either must be specified. - The number of possible edges grows exponentially as :math:`\approx 2^N for large `N` and + The number of possible edges grows exponentially as :math:`2^N` for large `N` and quickly becomes impractically long to compute, especially when using `max_order`. For - example, `N=100` and `max_order=5` already yields :math:`8\times 10^7` edges. Increasing `N=1000` - makes it :math:`8\times 10^{12}`. `N=100` and with a larger `max_order=6` yields :math:`8\times 10^8` edges. + example, `N=100` and `max_order=5` already yields :math:`10^8` edges. Increasing `N=1000` + makes it :math:`10^{13}`. `N=100` and with a larger `max_order=6` yields :math:`10^9` edges. """