From 7a1e85e63143ebe0b8d86ff0d16ce1ddec985ae2 Mon Sep 17 00:00:00 2001 From: Leo Torres Date: Wed, 10 May 2023 16:13:54 +0200 Subject: [PATCH] Try ruff (#346) Fix most ruff errors: fix line length problems, prefer function definition over assigning to lambdas, unused variables and related issues, and add some new tests. --- setup.py | 10 +- tests/algorithms/test_clustering.py | 1 - tests/algorithms/test_connected.py | 4 +- tests/classes/test_function.py | 28 +-- tests/classes/test_simplicialcomplex.py | 4 +- tests/drawing/test_layout.py | 19 +- tests/generators/test_classic.py | 2 +- tests/generators/test_nonuniform.py | 4 +- tests/stats/test_nodestats.py | 10 +- xgi/algorithms/centrality.py | 11 +- xgi/algorithms/clustering.py | 7 +- xgi/classes/function.py | 12 +- xgi/classes/hypergraph.py | 49 +++-- xgi/classes/hypergraphviews.py | 28 ++- xgi/classes/reportviews.py | 20 +- xgi/classes/simplicialcomplex.py | 103 +++++---- xgi/convert.py | 13 +- xgi/drawing/draw.py | 273 +++++++++++++----------- xgi/drawing/layout.py | 51 +++-- xgi/dynamics/synchronization.py | 26 +-- xgi/generators/classic.py | 17 +- xgi/generators/lattice.py | 5 +- xgi/generators/random.py | 7 +- xgi/generators/simplicial_complexes.py | 13 +- xgi/generators/uniform.py | 6 +- xgi/linalg/hodge_matrix.py | 15 +- xgi/linalg/hypergraph_matrix.py | 22 +- xgi/linalg/laplacian_matrix.py | 25 +-- xgi/readwrite/bipartite.py | 6 +- xgi/readwrite/incidence.py | 4 +- xgi/readwrite/json.py | 14 +- xgi/readwrite/xgi_data.py | 4 +- xgi/stats/__init__.py | 31 +-- xgi/stats/nodestats.py | 5 +- 34 files changed, 475 insertions(+), 374 deletions(-) diff --git a/setup.py b/setup.py index f43ed9417..5074805f6 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,15 @@ version = __version__ -authors = "Nicholas Landry, Leo Torres, Maxime Lucas, Iacopo Iacopini, Giovanni Petri, Alice Patania, and Alice Schwarze" +authors = ( + "Nicholas Landry, " + "Leo Torres, " + "Maxime Lucas, " + "Iacopo Iacopini, " + "Giovanni Petri, " + "Alice Patania, " + "and Alice Schwarze" +) author_email = "nicholas.landry@uvm.edu" diff --git a/tests/algorithms/test_clustering.py b/tests/algorithms/test_clustering.py index f74c30bcb..476a2e45e 100644 --- a/tests/algorithms/test_clustering.py +++ b/tests/algorithms/test_clustering.py @@ -1,4 +1,3 @@ -import numpy as np import pytest import xgi diff --git a/tests/algorithms/test_connected.py b/tests/algorithms/test_connected.py index 4b6c83266..cb1ecf08a 100644 --- a/tests/algorithms/test_connected.py +++ b/tests/algorithms/test_connected.py @@ -7,8 +7,8 @@ def test_is_connected(edgelist1, edgelist3): H1 = xgi.Hypergraph(edgelist1) H2 = xgi.Hypergraph(edgelist3) - assert xgi.is_connected(H1) == False - assert xgi.is_connected(H2) == True + assert xgi.is_connected(H1) is False + assert xgi.is_connected(H2) is True def test_connected_components(edgelist1, edgelist2, edgelist4): diff --git a/tests/classes/test_function.py b/tests/classes/test_function.py index b0b57f903..b1e0a5042 100644 --- a/tests/classes/test_function.py +++ b/tests/classes/test_function.py @@ -21,7 +21,7 @@ def test_max_edge_order(edgelist1, edgelist4, edgelist5): H3 = xgi.Hypergraph(edgelist4) H4 = xgi.Hypergraph(edgelist5) - assert xgi.max_edge_order(H0) == None + assert xgi.max_edge_order(H0) is None assert xgi.max_edge_order(H1) == 0 assert xgi.max_edge_order(H2) == 2 assert xgi.max_edge_order(H3) == 3 @@ -31,11 +31,11 @@ def test_max_edge_order(edgelist1, edgelist4, edgelist5): def test_is_possible_order(edgelist1): H1 = xgi.Hypergraph(edgelist1) - assert xgi.is_possible_order(H1, -1) == False - assert xgi.is_possible_order(H1, 0) == True - assert xgi.is_possible_order(H1, 1) == True - assert xgi.is_possible_order(H1, 2) == True - assert xgi.is_possible_order(H1, 3) == False + assert xgi.is_possible_order(H1, -1) is False + assert xgi.is_possible_order(H1, 0) is True + assert xgi.is_possible_order(H1, 1) is True + assert xgi.is_possible_order(H1, 2) is True + assert xgi.is_possible_order(H1, 3) is False def test_is_uniform(edgelist1, edgelist6, edgelist7): @@ -44,10 +44,10 @@ def test_is_uniform(edgelist1, edgelist6, edgelist7): H2 = xgi.Hypergraph(edgelist7) H3 = xgi.empty_hypergraph() - assert xgi.is_uniform(H0) == False + assert xgi.is_uniform(H0) is False assert xgi.is_uniform(H1) == 2 assert xgi.is_uniform(H2) == 2 - assert xgi.is_uniform(H3) == False + assert xgi.is_uniform(H3) is False def test_edge_neighborhood(edgelist3): @@ -157,7 +157,7 @@ def test_create_empty_copy(edgelist1): assert E2["name"] == "test" assert E2["timestamp"] == "Nov. 20" with pytest.raises(XGIError): - author = E2["author"] + _ = E2["author"] for n in E2.nodes: assert E2.nodes[n]["name"] == attr_dict[n]["name"] @@ -508,7 +508,9 @@ def test_density(edgelist1): with pytest.raises(ValueError): xgi.density(H, max_order=8) # max_order cannot be larger than number of nodes - dens_ignore_sing = lambda m: xgi.density(H, max_order=m, ignore_singletons=True) + def dens_ignore_sing(m): + return xgi.density(H, max_order=m, ignore_singletons=True) + assert abs(dens_ignore_sing(0) - 0.0) < tol assert abs(dens_ignore_sing(1) - 0.03571428571428571) < tol assert abs(dens_ignore_sing(2) - 0.03571428571428571) < tol @@ -678,9 +680,9 @@ def test_incidence_density(edgelist1): H, max_order=8 ) # max_order cannot be larger than number of nodes - dens_ignore_sing = lambda m: xgi.incidence_density( - H, max_order=m, ignore_singletons=True - ) + def dens_ignore_sing(m): + return xgi.incidence_density(H, max_order=m, ignore_singletons=True) + assert abs(dens_ignore_sing(0) - 0.0) < tol assert abs(dens_ignore_sing(1) - 0.25) < tol assert abs(dens_ignore_sing(2) - 1 / 3) < tol diff --git a/tests/classes/test_simplicialcomplex.py b/tests/classes/test_simplicialcomplex.py index 1a932d0cb..54b011bbb 100644 --- a/tests/classes/test_simplicialcomplex.py +++ b/tests/classes/test_simplicialcomplex.py @@ -12,9 +12,9 @@ def test_constructor(edgelist5, dict5, incidence5, dataframe5): S_sc = xgi.SimplicialComplex(S_list) with pytest.raises(XGIError): - S_dict = xgi.SimplicialComplex(dict5) + _ = xgi.SimplicialComplex(dict5) with pytest.raises(XGIError): - S_mat = xgi.SimplicialComplex(incidence5) + _ = xgi.SimplicialComplex(incidence5) assert set(S_list.nodes) == set(S_df.nodes) == set(S_sc.nodes) assert set(S_list.edges) == set(S_df.edges) == set(S_sc.edges) diff --git a/tests/drawing/test_layout.py b/tests/drawing/test_layout.py index cc51aafc8..3b088dd85 100644 --- a/tests/drawing/test_layout.py +++ b/tests/drawing/test_layout.py @@ -25,23 +25,30 @@ def test_random_layout(): assert len(pos) == S.num_nodes -def test_pairwise_spring_layout(): +def test_pairwise_spring_layout_hypergraph(): H = xgi.random_hypergraph(10, [0.2], seed=1) - # seed pos1 = xgi.pairwise_spring_layout(H, seed=1) pos2 = xgi.pairwise_spring_layout(H, seed=2) pos3 = xgi.pairwise_spring_layout(H, seed=2) - assert pos1.keys() == pos2.keys() - assert pos2.keys() == pos3.keys() + assert pos1.keys() == pos2.keys() == pos3.keys() assert not np.allclose(list(pos1.values()), list(pos2.values())) assert np.allclose(list(pos2.values()), list(pos3.values())) - assert len(pos1) == H.num_nodes # simplicial complex + + +def test_pairwise_spring_layout_simplicial_complex(): S = xgi.random_flag_complex_d2(10, 0.2, seed=1) - pos = xgi.pairwise_spring_layout(S, seed=1) + + pos1 = xgi.pairwise_spring_layout(S, seed=1) + pos2 = xgi.pairwise_spring_layout(S, seed=2) + pos3 = xgi.pairwise_spring_layout(S, seed=2) + assert pos1.keys() == pos2.keys() == pos3.keys() + assert not np.allclose(list(pos1.values()), list(pos2.values())) + assert np.allclose(list(pos2.values()), list(pos3.values())) + assert len(pos1) == S.num_nodes def test_barycenter_spring_layout(hypergraph1): diff --git a/tests/generators/test_classic.py b/tests/generators/test_classic.py index 4ca56c658..c24316a30 100644 --- a/tests/generators/test_classic.py +++ b/tests/generators/test_classic.py @@ -10,7 +10,7 @@ def test_empty_hypergraph(): assert (H.num_nodes, H.num_edges) == (0, 0) -def test_empty_hypergraph(): +def test_empty_simplicial_complex(): SC = xgi.empty_simplicial_complex() assert (SC.num_nodes, SC.num_edges) == (0, 0) diff --git a/tests/generators/test_nonuniform.py b/tests/generators/test_nonuniform.py index 712980ae5..856f9eb20 100644 --- a/tests/generators/test_nonuniform.py +++ b/tests/generators/test_nonuniform.py @@ -20,9 +20,7 @@ def test_chung_lu_hypergraph(): assert H2._edge == H3._edge with pytest.warns(Warning): - k1 = {1: 1, 2: 2} - k2 = {1: 2, 1: 2} - H = xgi.chung_lu_hypergraph(k1, k2) + _ = xgi.chung_lu_hypergraph({1: 1, 2: 2}, {1: 2, 2: 2}) def test_dcsbm_hypergraph(): diff --git a/tests/stats/test_nodestats.py b/tests/stats/test_nodestats.py index f4fcd8108..27fd1f5d3 100644 --- a/tests/stats/test_nodestats.py +++ b/tests/stats/test_nodestats.py @@ -75,15 +75,9 @@ def test_call_filterby(edgelist1, edgelist8): assert filtered.asdict() == {1: 3.5} -def test_filterby_attr(hyperwithattrs, attr1, attr2, attr3, attr4, attr5): +def test_filterby_attr(hyperwithattrs): H = hyperwithattrs - attrs = { - 1: attr1, - 2: attr2, - 3: attr3, - 4: attr4, - 5: attr5, - } + filtered = H.nodes.filterby_attr("age", 20, "eq") assert set(filtered) == {4} diff --git a/xgi/algorithms/centrality.py b/xgi/algorithms/centrality.py index d9eaa23ea..5105e65d6 100644 --- a/xgi/algorithms/centrality.py +++ b/xgi/algorithms/centrality.py @@ -112,8 +112,8 @@ def h_eigenvector_centrality(H, max_iter=100, tol=1e-6): new_H = convert_labels_to_integers(H, "old-label") - f = lambda v, m: np.power(v, 1.0 / m) - g = lambda v, x: np.prod(v[list(x)]) + f = lambda v, m: np.power(v, 1.0 / m) # noqa: E731 + g = lambda v, x: np.prod(v[list(x)]) # noqa: E731 x = np.random.uniform(size=(new_H.num_nodes)) x = x / norm(x, 1) @@ -263,10 +263,11 @@ def line_vector_centrality(H): References ---------- - Vector centrality in hypergraphs, - K. Kovalenko, M. Romance, E. Vasilyeva, D. Aleja, R. Criado, D. Musatov, - A.M. Raigorodskii, J. Flores, I. Samoylenko, K. Alfaro-Bittner, M. Perc, S. Boccaletti, + "Vector centrality in hypergraphs", K. Kovalenko, M. Romance, E. Vasilyeva, + D. Aleja, R. Criado, D. Musatov, A.M. Raigorodskii, J. Flores, I. Samoylenko, + K. Alfaro-Bittner, M. Perc, S. Boccaletti, https://doi.org/10.1016/j.chaos.2022.112397 + """ from ..algorithms import is_connected diff --git a/xgi/algorithms/clustering.py b/xgi/algorithms/clustering.py index afb9408d5..a5e646e97 100644 --- a/xgi/algorithms/clustering.py +++ b/xgi/algorithms/clustering.py @@ -103,8 +103,8 @@ def local_clustering_coefficient(H): References ---------- - "Properties of metabolic graphs: biological organization or representation artifacts?" - by Wanding Zhou and Luay Nakhleh. + "Properties of metabolic graphs: biological organization or representation + artifacts?" by Wanding Zhou and Luay Nakhleh. https://doi.org/10.1186/1471-2105-12-132 "Hypergraphs for predicting essential genes using multiprotein complex data" @@ -118,6 +118,7 @@ def local_clustering_coefficient(H): >>> cc = xgi.local_clustering_coefficient(H) >>> cc {0: 1.0, 1: 1.0, 2: 1.0} + """ result = {} @@ -147,7 +148,7 @@ def local_clustering_coefficient(H): # the neighbours of D1 and D2, respectively. neighD1 = {i for d in D1 for i in H.nodes.neighbors(d)} neighD2 = {i for d in D2 for i in H.nodes.neighbors(d)} - # compute the extra overlap [len() is used for cardinality of edges] + # compute extra overlap [len() is used for cardinality of edges] eo = ( len(neighD1.intersection(D2)) + len(neighD2.intersection(D1)) diff --git a/xgi/classes/function.py b/xgi/classes/function.py index aed2eb8e5..ae247483b 100644 --- a/xgi/classes/function.py +++ b/xgi/classes/function.py @@ -146,9 +146,9 @@ def is_uniform(H): def edge_neighborhood(H, n, include_self=False): """The edge neighborhood of the specified node. - The edge neighborhood of a node `n` in a hypergraph `H` is an edgelist of all the edges - containing `n` and its edges are all the edges in `H` that contain - `n`. Usually, the edge neighborhood does not include `n` itself. This can be controlled + The edge neighborhood of a node `n` in a hypergraph `H` is an edgelist of all the + edges containing `n` and its edges are all the edges in `H` that contain `n`. + Usually, the edge neighborhood does not include `n` itself. This can be controlled with `include_self`. Parameters @@ -573,7 +573,8 @@ def set_edge_attributes(H, values, name=None): warn(f"Edge {e} does not exist!") except AttributeError: raise XGIError( - "name property has not been set and a dict-of-dicts has not been provided." + "name property has not been set and a " + "dict-of-dicts has not been provided." ) @@ -882,7 +883,8 @@ def subfaces(edges, order=None): max_order = len(max(edges, key=len)) - 1 if order and order > max_order: raise XGIError( - f"order must be less or equal to the maximum order among the edges: {max_order}." + "order must be less or equal to the maximum " + f"order among the edges: {max_order}." ) faces = [] diff --git a/xgi/classes/hypergraph.py b/xgi/classes/hypergraph.py index 32381d78d..6bf018e44 100644 --- a/xgi/classes/hypergraph.py +++ b/xgi/classes/hypergraph.py @@ -149,9 +149,15 @@ def __str__(self): """ try: - return f"{type(self).__name__} named {self['name']} with {self.num_nodes} nodes and {self.num_edges} hyperedges" + return ( + f"{type(self).__name__} named {self['name']} " + f"with {self.num_nodes} nodes and {self.num_edges} hyperedges" + ) except XGIError: - return f"Unnamed {type(self).__name__} with {self.num_nodes} nodes and {self.num_edges} hyperedges" + return ( + f"Unnamed {type(self).__name__} with " + f"{self.num_nodes} nodes and {self.num_edges} hyperedges" + ) def __iter__(self): """Iterate over the nodes. @@ -217,7 +223,8 @@ def __getattr__(self, attr): if stat is None: word = None raise AttributeError( - f"{attr} is not a method of Hypergraph or a recognized NodeStat or EdgeStat" + f"{attr} is not a method of Hypergraph or a " + "recognized NodeStat or EdgeStat" ) def func(node=None, *args, **kwargs): @@ -528,29 +535,29 @@ def add_edge(self, members, id=None, **attr): update_uid_counter(self, id) def add_edges_from(self, ebunch_to_add, **attr): - """Add multiple edges with optional attributes. + r"""Add multiple edges with optional attributes. Parameters ---------- ebunch_to_add : Iterable An iterable of edges. This may be an iterable of iterables (Format 1), - where each element contains the members of the edge specified as valid node IDs. - Alternatively, each element could also be a tuple in any of the following - formats: + where each element contains the members of the edge specified as valid node + IDs. Alternatively, each element could also be a tuple in any of the + following formats: * Format 2: 2-tuple (members, edge_id), or * Format 3: 2-tuple (members, attr), or * Format 4: 3-tuple (members, edge_id, attr), where `members` is an iterable of node IDs, `edge_id` is a hashable to use - as edge ID, and `attr` is a dict of attributes. Finally, `ebunch_to_add` - may be a dict of the form `{edge_id: edge_members}` (Format 5). + as edge ID, and `attr` is a dict of attributes. Finally, `ebunch_to_add` may + be a dict of the form `{edge_id: edge_members}` (Format 5). - Formats 2 and 3 are unambiguous because `attr` dicts are not hashable, while `id`s must be. - In Formats 2-4, each element of `ebunch_to_add` must have the same length, - i.e. you cannot mix different formats. The iterables containing edge - members cannot be strings. + Formats 2 and 3 are unambiguous because `attr` dicts are not hashable, while + `id`s must be. In Formats 2-4, each element of `ebunch_to_add` must have + the same length, i.e. you cannot mix different formats. The iterables + containing edge members cannot be strings. attr : \*\*kwargs, optional Additional attributes to be assigned to all edges. Attribues specified via @@ -1172,7 +1179,8 @@ def merge_duplicate_edges( if merge_rule == "union": warn( - "You will not be able to color/draw by merged attributes with xgi.draw()!" + "You will not be able to color/draw by " + "merged attributes with xgi.draw()!" ) def copy(self): @@ -1235,15 +1243,18 @@ def cleanup( Parameters ---------- isolates : bool, optional - Whether isolated nodes are allowed, by default False + Whether isolated nodes are allowed, by default False. singletons : bool, optional - Whether singleton edges are allowed, by default False + Whether singleton edges are allowed, by default False. multiedges : bool, optional - Whether multiedges are allowed, by default False + Whether multiedges are allowed, by default False. relabel : bool, optional - Whether to convert all node and edge labels to sequential integers, by default True + Whether to convert all node and edge labels to sequential integers, by + default True. in_place : bool, optional - Whether to modify the current hypergraph or output a new one, by default True + Whether to modify the current hypergraph or output a new one, by default + True. + """ if in_place: if not multiedges: diff --git a/xgi/classes/hypergraphviews.py b/xgi/classes/hypergraphviews.py index 116254db4..6a18199f3 100644 --- a/xgi/classes/hypergraphviews.py +++ b/xgi/classes/hypergraphviews.py @@ -1,11 +1,11 @@ """View of Hypergraphs as a subhypergraph or read-only. -In some algorithms it is convenient to temporarily morph -a hypergraph to exclude some nodes or edges. It should be better -to do that via a view than to remove and then re-add. This module provides those graph views. +In some algorithms it is convenient to temporarily morph a hypergraph to exclude some +nodes or edges. It should be better to do that via a view than to remove and then +re-add. This module provides those graph views. -The resulting views are essentially read-only graphs that -report data from the original graph object. +The resulting views are essentially read-only graphs that report data from the original +graph object. Note: Since hypergraphviews look like hypergraphs, one can end up with view-of-view-of-view chains. Be careful with chains because they become very slow with @@ -23,18 +23,18 @@ def subhypergraph(H, nodes=None, edges=None, keep_isolates=True): `subhypergraph_view` provides a read-only view of the induced subhypergraph that includes nodes, edges, or both based on what the user specifies. This function - automatically filters out edges that are not subsets of the nodes. This function - may create isolated nodes. + automatically filters out edges that are not subsets of the nodes. This function may + create isolated nodes. - If the user only specifies the nodes to include, the function returns - an induced subhypergraph on the nodes. + If the user only specifies the nodes to include, the function returns an induced + subhypergraph on the nodes. - If the user only specifies the edges to include, the function returns all of the nodes - and the specified edges. + If the user only specifies the edges to include, the function returns all of the + nodes and the specified edges. If the user specifies both nodes and edges to include in the subhypergraph, then the - function returns a subhypergraph with the specified nodes and edges from the list of specified - hyperedges that are induced by the specified nodes. + function returns a subhypergraph with the specified nodes and edges from the list of + specified hyperedges that are induced by the specified nodes. Parameters ---------- @@ -55,8 +55,6 @@ def subhypergraph(H, nodes=None, edges=None, keep_isolates=True): Hypergraph object A read-only hypergraph view of the input hypergraph. - - """ new = H.__class__() diff --git a/xgi/classes/reportviews.py b/xgi/classes/reportviews.py index 4509f5603..4515f2d35 100644 --- a/xgi/classes/reportviews.py +++ b/xgi/classes/reportviews.py @@ -262,7 +262,8 @@ def filterby(self, stat, val, mode="eq"): bunch = [node for node in self if val[0] <= values[node] <= val[1]] else: raise ValueError( - f"Unrecognized mode {mode}. mode must be one of 'eq', 'neq', 'lt', 'gt', 'leq', 'geq', or 'between'." + f"Unrecognized mode {mode}. mode must be one of " + "'eq', 'neq', 'lt', 'gt', 'leq', 'geq', or 'between'." ) return type(self).from_view(self, bunch) @@ -329,7 +330,8 @@ def filterby_attr(self, attr, val, mode="eq", missing=None): ] else: raise ValueError( - f"Unrecognized mode {mode}. mode must be one of 'eq', 'neq', 'lt', 'gt', 'leq', 'geq', or 'between'." + f"Unrecognized mode {mode}. mode must be one of " + "'eq', 'neq', 'lt', 'gt', 'leq', 'geq', or 'between'." ) return type(self).from_view(self, bunch) @@ -577,13 +579,13 @@ def memberships(self, n=None): def isolates(self, ignore_singletons=False): """Nodes that belong to no edges. - When ignore_singletons is True, a node is considered isolated from the - rest of the hypergraph when it is included in no edges of size two or more. In + When ignore_singletons is True, a node is considered isolated from the rest of + the hypergraph when it is included in no edges of size two or more. In particular, whether the node is part of any singleton edges is irrelevant to determine whether it is isolated. - When ignore_singletons is False (default), a node is isolated only when it is a member of - exactly zero edges, including singletons. + When ignore_singletons is False (default), a node is isolated only when it is a + member of exactly zero edges, including singletons. Parameters ---------- @@ -751,11 +753,9 @@ def maximal(self, strict=False): nodes = self._bi_id_dict max_edges = set() - _intersection = lambda x, y: x & y - if strict: for i, e in edges.items(): - if reduce(_intersection, (nodes[n] for n in e)) == {i}: + if reduce(lambda x, y: x & y, (nodes[n] for n in e)) == {i}: max_edges.add(i) else: # This data structure so that the algorithm can handle multi-edges @@ -767,7 +767,7 @@ def maximal(self, strict=False): # If a multi-edge has already been added to the set of # maximal edges, we don't need to check. if i not in max_edges: - if reduce(_intersection, (nodes[n] for n in e)) == set( + if reduce(lambda x, y: x & y, (nodes[n] for n in e)) == set( dups[frozenset(e)] ): max_edges.update(dups[frozenset(e)]) diff --git a/xgi/classes/simplicialcomplex.py b/xgi/classes/simplicialcomplex.py index 6230daf0f..bc63028db 100644 --- a/xgi/classes/simplicialcomplex.py +++ b/xgi/classes/simplicialcomplex.py @@ -1,9 +1,10 @@ """Base class for undirected simplicial complexes. -The SimplicialComplex class allows any hashable object as a node -and can associate key/value attribute pairs with each undirected simplex and node. +The SimplicialComplex class allows any hashable object as a node and can associate +key/value attribute pairs with each undirected simplex and node. Multi-simplices are not allowed. + """ from collections.abc import Hashable, Iterable @@ -22,24 +23,23 @@ class SimplicialComplex(Hypergraph): r"""A class to represent undirected simplicial complexes. A simplicial complex is a collection of subsets of a set of *nodes* or *vertices*. - It is a pair :math:`(V, E)`, where :math:`V` is a set of elements called - *nodes* or *vertices*, and :math:`E` is a set whose elements are subsets of - :math:`V`, that is, each :math:`e \in E` satisfies :math:`e \subset V`. The - elements of :math:`E` are called *simplices*. Additionally, if a simplex is part of - a simplicial complex, all its faces must be too. This makes simplicial complexes - a special case of hypergraphs. + It is a pair :math:`(V, E)`, where :math:`V` is a set of elements called *nodes* or + *vertices*, and :math:`E` is a set whose elements are subsets of :math:`V`, that is, + each :math:`e \in E` satisfies :math:`e \subset V`. The elements of :math:`E` are + called *simplices*. Additionally, if a simplex is part of a simplicial complex, all + its faces must be too. This makes simplicial complexes a special case of + hypergraphs. The SimplicialComplex class allows any hashable object as a node and can associate - attributes to each node, simplex, or the simplicial complex itself, in the form of key/value - pairs. - + attributes to each node, simplex, or the simplicial complex itself, in the form of + key/value pairs. Parameters ---------- incoming_data : input simplicial complex data, optional Data to initialize the simplicial complex. If None (default), an empty - simplicial complex is created, i.e. one with no nodes or simplices. - The data can be in the following formats: + simplicial complex is created, i.e. one with no nodes or simplices. The data + can be in the following formats: * simplex list * simplex dictionary @@ -110,44 +110,54 @@ def __str__(self): """ try: - return f"{type(self).__name__} named '{self['name']}' with {self.num_nodes} nodes and {self.num_edges} simplices" + return ( + f"{type(self).__name__} named '{self['name']}' " + f"with {self.num_nodes} nodes and {self.num_edges} simplices" + ) except XGIError: - return f"Unnamed {type(self).__name__} with {self.num_nodes} nodes and {self.num_edges} simplices" + return ( + f"Unnamed {type(self).__name__} with {self.num_nodes}" + f" nodes and {self.num_edges} simplices" + ) def add_edge(self, edge, id=None, **attr): - """add_edge is deprecated in SimplicialComplex. Use add_simplex instead""" + """Deprecated in SimplicialComplex. Use add_simplex instead""" warn("add_edge is deprecated in SimplicialComplex. Use add_simplex instead") return self.add_simplex(edge, id=None, **attr) def add_edges_from(self, ebunch_to_add, max_order=None, **attr): - """add_edges_from is deprecated in SimplicialComplex. Use add_simplices_from instead""" + """Deprecated in SimplicialComplex. Use add_simplices_from instead""" warn( - "add_edges_from is deprecated in SimplicialComplex. Use add_simplices_from instead" + "add_edges_from is deprecated in SimplicialComplex. " + "Use add_simplices_from instead" ) return self.add_simplices_from(ebunch_to_add, max_order=None, **attr) def add_weighted_edges_from( self, ebunch_to_add, max_order=None, weight="weight", **attr ): - """add_weighted_edges_from is deprecated in SimplicialComplex. Use add_weighted_simplices_from instead""" + """Deprecated in SimplicialComplex. Use add_weighted_simplices_from instead""" warn( - "add_weighted_edges_from is deprecated in SimplicialComplex. Use add_weighted_simplices_from instead" + "add_weighted_edges_from is deprecated in SimplicialComplex." + " Use add_weighted_simplices_from instead" ) return self.add_weighted_simplices_from( ebunch_to_add, max_order=max_order, weight=weight, **attr ) def remove_edge(self, id): - """remove_edge is deprecated in SimplicialComplex. Use remove_simplex_id instead""" + """Deprecated in SimplicialComplex. Use remove_simplex_id instead""" warn( - "remove_edge is deprecated in SimplicialComplex. Use remove_simplex_id instead" + "remove_edge is deprecated in SimplicialComplex. " + "Use remove_simplex_id instead" ) return self.remove_simplex_id(id) def remove_edges_from(self, ebunch): - """remove_edges_from is deprecated in SimplicialComplex. Use remove_simplex_ids_from instead""" + """Deprecated in SimplicialComplex. Use remove_simplex_ids_from instead""" warn( - "remove_edges_from is deprecated in SimplicialComplex. Use remove_simplex_ids_from instead" + "remove_edges_from is deprecated in SimplicialComplex. " + "Use remove_simplex_ids_from instead" ) return self.remove_simplex_ids_from(ebunch) @@ -204,7 +214,8 @@ def add_simplex(self, members, id=None, **attr): members : Iterable An iterable of the ids of the nodes contained in the new simplex. id : hashable, optional - Id of the new simplex. If None (default), a unique numeric ID will be created. + Id of the new simplex. If None (default), a unique numeric ID will be + created. **attr : dict, optional Attributes of the new simplex. @@ -245,6 +256,7 @@ def add_simplex(self, members, id=None, **attr): EdgeView((0, 1, 2, 3, 'myedge', 4)) >>> S.edges[4] {'color': 'red', 'place': 'peru'} + """ try: @@ -324,32 +336,32 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): Parameters ---------- ebunch_to_add : Iterable - An iterable of simplices. This may be an iterable of iterables (Format 1), - where each element contains the members of the simplex specified as valid node IDs. - Alternatively, each element could also be a tuple in any of the following - formats: + where each element contains the members of the simplex specified as valid + node IDs. Alternatively, each element could also be a tuple in any of the + following formats: * Format 2: 2-tuple (members, simplex_id), or * Format 3: 2-tuple (members, attr), or * Format 4: 3-tuple (members, simplex_id, attr), - where `members` is an iterable of node IDs, `simplex_id` is a hashable to use - as simplex ID, and `attr` is a dict of attributes. Finally, `ebunch_to_add` - may be a dict of the form `{simplex_id: simplex_members}` (Format 5). + where `members` is an iterable of node IDs, `simplex_id` is a hashable to + use as simplex ID, and `attr` is a dict of attributes. Finally, + `ebunch_to_add` may be a dict of the form `{simplex_id: simplex_members}` + (Format 5). - Formats 2 and 3 are unambiguous because `attr` dicts are not hashable, while `id`s must be. - In Formats 2-4, each element of `ebunch_to_add` must have the same length, - i.e. you cannot mix different formats. The iterables containing simplex - members cannot be strings. + Formats 2 and 3 are unambiguous because `attr` dicts are not hashable, while + `id`s must be. In Formats 2-4, each element of `ebunch_to_add` must have + the same length, i.e. you cannot mix different formats. The iterables + containing simplex members cannot be strings. max_order : int, optional - Maximal dimension of simplices to add. If None (default), adds all simplices. - If int, and `ebunch_to_add` contains simplices of order > `max_order`, creates - and adds all its subfaces up to `max_order`. + Maximal dimension of simplices to add. If None (default), adds all + simplices. If int, and `ebunch_to_add` contains simplices of order > + `max_order`, creates and adds all its subfaces up to `max_order`. attr : \*\*kwargs, optional - Additional attributes to be assigned to all simplices. Attribues specified via - `ebunch_to_add` take precedence over `attr`. + Additional attributes to be assigned to all simplices. Attribues specified + via `ebunch_to_add` take precedence over `attr`. See Also -------- @@ -366,8 +378,8 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): >>> import xgi >>> S = xgi.SimplicialComplex() - When specifying simplices by their members only, numeric simplex IDs will be assigned - automatically. + When specifying simplices by their members only, numeric simplex IDs will be + assigned automatically. >>> S.add_simplices_from([[0, 1], [1, 2], [2, 3, 4]]) >>> S.edges.members(dtype=dict) # doctest: +SKIP @@ -433,7 +445,7 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): warn(f"uid {id} already exists, cannot add simplex {members}.") continue - if max_order != None: + if max_order is not None: if len(members) > max_order + 1: combos = powerset(members, include_singletons=False) faces += list(combos) @@ -527,7 +539,7 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): if format1 or format3: id = next(self._edge_uid) - if max_order != None: + if max_order is not None: if len(members) > max_order + 1: combos = powerset(members, include_singletons=False) faces += list(combos) # store faces @@ -595,6 +607,7 @@ def close(self): ----- Adding the same simplex twice will add it only once. Currently cannot add empty simplices; the method skips over them. + """ ebunch_to_close = list(map(list, self.edges.members())) for simplex in ebunch_to_close: diff --git a/xgi/convert.py b/xgi/convert.py index 3bc20a2e2..074e31c57 100644 --- a/xgi/convert.py +++ b/xgi/convert.py @@ -61,7 +61,8 @@ def convert_to_hypergraph(data, create_using=None): * numpy ndarray * scipy sparse matrix create_using : Hypergraph constructor, optional (default=Hypergraph) - Hypergraph type to create. If hypergraph instance, then cleared before populated. + Hypergraph type to create. If hypergraph instance, then cleared before + populated. Returns ------- @@ -164,11 +165,11 @@ def to_line_graph(H, s=1): References ---------- - "Hypernetwork science via high-order hypergraph walks" - by Sinan G. Aksoy, Cliff Joslyn, Carlos Ortiz Marrero, Brenda Praggastis & Emilie Purvine. + "Hypernetwork science via high-order hypergraph walks", by Sinan G. Aksoy, Cliff + Joslyn, Carlos Ortiz Marrero, Brenda Praggastis & Emilie Purvine. https://doi.org/10.1140/epjds/s13688-020-00231-0 - """ + """ LG = nx.Graph() edge_label_dict = {tuple(edge): index for index, edge in H._edge.items()} @@ -200,12 +201,14 @@ def convert_to_simplicial_complex(data, create_using=None): * numpy ndarray * scipy sparse matrix create_using : Hypergraph graph constructor, optional (default=Hypergraph) - Hypergraph type to create. If hypergraph instance, then cleared before populated. + Hypergraph type to create. If hypergraph instance, then cleared before + populated. Returns ------- Hypergraph object A hypergraph constructed from the data + """ if data is None: diff --git a/xgi/drawing/draw.py b/xgi/drawing/draw.py index 684d6567d..8d9a7e058 100644 --- a/xgi/drawing/draw.py +++ b/xgi/drawing/draw.py @@ -50,54 +50,59 @@ def draw( H : Hypergraph or SimplicialComplex. Hypergraph to draw pos : dict, optional - If passed, this dictionary of positions node_id:(x,y) is used for placing the 0-simplices. - If None (default), use the `barycenter_spring_layout` to compute the positions. + If passed, this dictionary of positions node_id:(x,y) is used for placing the + 0-simplices. If None (default), use the `barycenter_spring_layout` to compute + the positions. ax : matplotlib.pyplot.axes, optional Axis to draw on. If None (default), get the current axes. dyad_color : str, dict, iterable, or EdgeStat, optional - Color of the dyadic links. If str, use the same color for all edges. If a dict, must - contain (edge_id: color_str) pairs. If iterable, assume the colors are - specified in the same order as the edges are found in H.edges. If EdgeStat, use a colormap - (specified with dyad_color_cmap) associated to it. By default, "black". + Color of the dyadic links. If str, use the same color for all edges. If a dict, + must contain (edge_id: color_str) pairs. If iterable, assume the colors are + specified in the same order as the edges are found in H.edges. If EdgeStat, use + a colormap (specified with dyad_color_cmap) associated to it. By default, + "black". dyad_lw : int, float, dict, iterable, or EdgeStat, optional - Line width of edges of order 1 (dyadic links). If int or float, use the same width for all edges. - If a dict, must contain (edge_id: width) pairs. If iterable, assume the widths are - specified in the same order as the edges are found in H.edges. If EdgeStat, use a monotonic - linear interpolation defined between min_dyad_lw and max_dyad_lw. By default, 1.5. + Line width of edges of order 1 (dyadic links). If int or float, use the same + width for all edges. If a dict, must contain (edge_id: width) pairs. If + iterable, assume the widths are specified in the same order as the edges are + found in H.edges. If EdgeStat, use a monotonic linear interpolation defined + between min_dyad_lw and max_dyad_lw. By default, 1.5. edge_fc : str, dict, iterable, or EdgeStat, optional - Color of the hyperedges. If str, use the same color for all nodes. If a dict, must - contain (edge_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the hyperedges are found in H.edges. If EdgeStat, - use the colormap specified with edge_fc_cmap. If None (default), - use the H.edges.size. + Color of the hyperedges. If str, use the same color for all nodes. If a dict, + must contain (edge_id: color_str) pairs. If other iterable, assume the colors + are specified in the same order as the hyperedges are found in H.edges. If + EdgeStat, use the colormap specified with edge_fc_cmap. If None (default), use + the H.edges.size. node_fc : str, dict, iterable, or NodeStat, optional Color of the nodes. If str, use the same color for all nodes. If a dict, must contain (node_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the nodes are found in H.nodes. If NodeStat, - use the colormap specified with node_fc_cmap. By default, "white". + specified in the same order as the nodes are found in H.nodes. If NodeStat, use + the colormap specified with node_fc_cmap. By default, "white". node_ec : str, dict, iterable, or NodeStat, optional - Color of node borders. If str, use the same color for all nodes. If a dict, must - contain (node_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the nodes are found in H.nodes. If NodeStat, + Color of node borders. If str, use the same color for all nodes. If a dict, + must contain (node_id: color_str) pairs. If other iterable, assume the colors + are specified in the same order as the nodes are found in H.nodes. If NodeStat, use the colormap specified with node_ec_cmap. By default, "black". node_lw : int, float, dict, iterable, or NodeStat, optional - Line width of the node borders in pixels. If int or float, use the same width for all node borders. - If a dict, must contain (node_id: width) pairs. If iterable, assume the widths are - specified in the same order as the nodes are found in H.nodes. If NodeStat, use a monotonic - linear interpolation defined between min_node_lw and max_node_lw. By default, 1. + Line width of the node borders in pixels. If int or float, use the same width + for all node borders. If a dict, must contain (node_id: width) pairs. If + iterable, assume the widths are specified in the same order as the nodes are + found in H.nodes. If NodeStat, use a monotonic linear interpolation defined + between min_node_lw and max_node_lw. By default, 1. node_size : int, float, dict, iterable, or NodeStat, optional - Radius of the nodes in pixels. If int or float, use the same radius for all nodes. - If a dict, must contain (node_id: radius) pairs. If iterable, assume the radiuses are - specified in the same order as the nodes are found in H.nodes. If NodeStat, use a monotonic - linear interpolation defined between min_node_size and max_node_size. By default, 15. + Radius of the nodes in pixels. If int or float, use the same radius for all + nodes. If a dict, must contain (node_id: radius) pairs. If iterable, assume + the radiuses are specified in the same order as the nodes are found in + H.nodes. If NodeStat, use a monotonic linear interpolation defined between + min_node_size and max_node_size. By default, 15. max_order : int, optional Maximum of hyperedges to plot. If None (default), plots all orders. node_labels : bool or dict, optional If True, draw ids on the nodes. If a dict, must contain (node_id: label) pairs. By default, False. hyperedge_labels : bool or dict, optional - If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) pairs. - By default, False. + If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) + pairs. By default, False. **kwargs : optional args Alternate default values. Values that can be overwritten are the following: * min_node_size @@ -125,6 +130,7 @@ def draw( draw_simplices draw_node_labels draw_hyperedge_labels + """ settings = { "min_node_size": 10.0, @@ -228,28 +234,31 @@ def draw_nodes( ax : matplotlib.pyplot.axes, optional Axis to draw on. If None (default), get the current axes. pos : dict, optional - If passed, this dictionary of positions node_id:(x,y) is used for placing the 0-simplices. - If None (default), use the `barycenter_spring_layout` to compute the positions. + If passed, this dictionary of positions node_id:(x,y) is used for placing the + 0-simplices. If None (default), use the `barycenter_spring_layout` to compute + the positions. node_fc : str, dict, iterable, or NodeStat, optional Color of the nodes. If str, use the same color for all nodes. If a dict, must contain (node_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the nodes are found in H.nodes. If NodeStat, - use the colormap specified with node_fc_cmap. By default, "white". + specified in the same order as the nodes are found in H.nodes. If NodeStat, use + the colormap specified with node_fc_cmap. By default, "white". node_ec : str, dict, iterable, or NodeStat, optional - Color of node borders. If str, use the same color for all nodes. If a dict, must - contain (node_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the nodes are found in H.nodes. If NodeStat, + Color of node borders. If str, use the same color for all nodes. If a dict, + must contain (node_id: color_str) pairs. If other iterable, assume the colors + are specified in the same order as the nodes are found in H.nodes. If NodeStat, use the colormap specified with node_ec_cmap. By default, "black". node_lw : int, float, dict, iterable, or EdgeStat, optional - Line width of the node borders in pixels. If int or float, use the same width for all node borders. - If a dict, must contain (node_id: width) pairs. If iterable, assume the widths are - specified in the same order as the nodes are found in H.nodes. If NodeStat, use a monotonic - linear interpolation defined between min_node_lw and max_node_lw. By default, 1. + Line width of the node borders in pixels. If int or float, use the same width + for all node borders. If a dict, must contain (node_id: width) pairs. If + iterable, assume the widths are specified in the same order as the nodes are + found in H.nodes. If NodeStat, use a monotonic linear interpolation defined + between min_node_lw and max_node_lw. By default, 1. node_size : int, float, dict, iterable, or NodeStat, optional - Radius of the nodes in pixels. If int or float, use the same radius for all nodes. - If a dict, must contain (node_id: radius) pairs. If iterable, assume the radiuses are - specified in the same order as the nodes are found in H.nodes. If NodeStat, use a monotonic - linear interpolation defined between min_node_size and max_node_size. By default, 15. + Radius of the nodes in pixels. If int or float, use the same radius for all + nodes. If a dict, must contain (node_id: radius) pairs. If iterable, assume + the radiuses are specified in the same order as the nodes are found in + H.nodes. If NodeStat, use a monotonic linear interpolation defined between + min_node_size and max_node_size. By default, 15. zorder : int The layer on which to draw the nodes. node_labels : bool or dict @@ -272,6 +281,7 @@ def draw_nodes( draw_simplices draw_node_labels draw_hyperedge_labels + """ if settings is None: @@ -361,28 +371,32 @@ def draw_hyperedges( ax : matplotlib.pyplot.axes, optional Axis to draw on. If None (default), get the current axes. pos : dict, optional - If passed, this dictionary of positions node_id:(x,y) is used for placing the 0-simplices. - If None (default), use the `barycenter_spring_layout` to compute the positions. + If passed, this dictionary of positions node_id:(x,y) is used for placing the + 0-simplices. If None (default), use the `barycenter_spring_layout` to compute + the positions. dyad_color : str, dict, iterable, or EdgeStat, optional - Color of the dyadic links. If str, use the same color for all edges. If a dict, must - contain (edge_id: color_str) pairs. If iterable, assume the colors are - specified in the same order as the edges are found in H.edges. If EdgeStat, use a colormap - (specified with dyad_color_cmap) associated to it. By default, "black". + Color of the dyadic links. If str, use the same color for all edges. If a dict, + must contain (edge_id: color_str) pairs. If iterable, assume the colors are + specified in the same order as the edges are found in H.edges. If EdgeStat, use + a colormap (specified with dyad_color_cmap) associated to it. By default, + "black". dyad_lw : int, float, dict, iterable, or EdgeStat, optional - Line width of edges of order 1 (dyadic links). If int or float, use the same width for all edges. - If a dict, must contain (edge_id: width) pairs. If iterable, assume the widths are - specified in the same order as the edges are found in H.edges. If EdgeStat, use a monotonic - linear interpolation defined between min_dyad_lw and max_dyad_lw. By default, 1.5. + Line width of edges of order 1 (dyadic links). If int or float, use the same + width for all edges. If a dict, must contain (edge_id: width) pairs. If + iterable, assume the widths are specified in the same order as the edges are + found in H.edges. If EdgeStat, use a monotonic linear interpolation defined + between min_dyad_lw and max_dyad_lw. By default, 1.5. edge_fc : str, dict, iterable, or EdgeStat, optional - Color of the hyperedges. If str, use the same color for all nodes. If a dict, must - contain (edge_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the hyperedges are found in H.edges. If EdgeStat, - use the colormap specified with edge_fc_cmap. If None (default), color by edge size. + Color of the hyperedges. If str, use the same color for all nodes. If a dict, + must contain (edge_id: color_str) pairs. If other iterable, assume the colors + are specified in the same order as the hyperedges are found in H.edges. If + EdgeStat, use the colormap specified with edge_fc_cmap. If None (default), color + by edge size. max_order : int, optional Maximum of hyperedges to plot. By default, None. hyperedge_labels : bool or dict, optional - If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) pairs. - By default, None. + If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) + pairs. By default, None. settings : dict Default parameters. Keys that may be useful to override default settings: * min_dyad_lw @@ -404,6 +418,7 @@ def draw_hyperedges( draw_simplices draw_node_labels draw_hyperedge_labels + """ if pos is None: @@ -439,7 +454,8 @@ def draw_hyperedges( edge_fc = _color_arg_to_dict(edge_fc, H.edges, settings["edge_fc_cmap"]) - # Looping over the hyperedges of different order (reversed) -- nodes will be plotted separately + # Looping over the hyperedges of different order (reversed) -- nodes will be plotted + # separately for id, he in H.edges.members(dtype=dict).items(): d = len(he) - 1 if d > max_order: @@ -509,30 +525,35 @@ def draw_simplices( ax : matplotlib.pyplot.axes, optional Axis to draw on. If None (default), get the current axes. pos : dict, optional - If passed, this dictionary of positions node_id:(x,y) is used for placing the 0-simplices. - If None (default), use the `barycenter_spring_layout` to compute the positions. + If passed, this dictionary of positions node_id:(x,y) is used for placing the + 0-simplices. If None (default), use the `barycenter_spring_layout` to compute + the positions. dyad_color : str, dict, iterable, or EdgeStat, optional - Color of the dyadic links. If str, use the same color for all edges. If a dict, must - contain (edge_id: color_str) pairs. If iterable, assume the colors are - specified in the same order as the edges are found in H.edges. If EdgeStat, use a colormap - (specified with dyad_color_cmap) associated to it. By default, "black". + Color of the dyadic links. If str, use the same color for all edges. If a dict, + must contain (edge_id: color_str) pairs. If iterable, assume the colors are + specified in the same order as the edges are found in H.edges. If EdgeStat, use + a colormap (specified with dyad_color_cmap) associated to it. By default, + "black". dyad_lw : int, float, dict, iterable, or EdgeStat, optional - Line width of edges of order 1 (dyadic links). If int or float, use the same width for all edges. - If a dict, must contain (edge_id: width) pairs. If iterable, assume the widths are - specified in the same order as the edges are found in H.edges. If EdgeStat, use a monotonic - linear interpolation defined between min_dyad_lw and max_dyad_lw. By default, 1.5. + Line width of edges of order 1 (dyadic links). If int or float, use the same + width for all edges. If a dict, must contain (edge_id: width) pairs. If + iterable, assume the widths are specified in the same order as the edges are + found in H.edges. If EdgeStat, use a monotonic linear interpolation defined + between min_dyad_lw and max_dyad_lw. By default, 1.5. edge_fc : str, dict, iterable, or EdgeStat, optional - Color of the hyperedges. If str, use the same color for all nodes. If a dict, must - contain (edge_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the hyperedges are found in H.edges. If EdgeStat, - use the colormap specified with edge_fc_cmap. If None (default), color by simplex size. + Color of the hyperedges. If str, use the same color for all nodes. If a dict, + must contain (edge_id: color_str) pairs. If other iterable, assume the colors + are specified in the same order as the hyperedges are found in H.edges. If + EdgeStat, use the colormap specified with edge_fc_cmap. If None (default), color + by simplex size. max_order : int, optional Maximum of hyperedges to plot. By default, None. hyperedge_labels : bool or dict, optional - If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) pairs. - Note, we plot only the maximal simplices so if you pass a dict be careful to match its keys - with the new edge ids in the converted SimplicialComplex. These may differ from the - edge ids in the given SC. By default, False. + If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) + pairs. Note, we plot only the maximal simplices so if you pass a dict be + careful to match its keys with the new edge ids in the converted + SimplicialComplex. These may differ from the edge ids in the given SC. By + default, False. settings : dict Default parameters. Keys that may be useful to override default settings: * min_dyad_lw @@ -555,6 +576,7 @@ def draw_simplices( draw_hyperedges draw_node_labels draw_hyperedge_labels + """ if max_order: @@ -600,7 +622,8 @@ def draw_simplices( edge_fc = _color_arg_to_dict(edge_fc, H_.edges, settings["edge_fc_cmap"]) - # Looping over the hyperedges of different order (reversed) -- nodes will be plotted separately + # Looping over the hyperedges of different order (reversed) -- nodes will be plotted + # separately for id, he in H_.edges.members(dtype=dict).items(): d = len(he) - 1 @@ -687,7 +710,8 @@ def _scalar_arg_to_dict(arg, ids, min_val, max_val): """ if isinstance(arg, str): raise TypeError( - f"Argument must be int, float, dict, iterable, or NodeStat/EdgeStat. Received {type(arg)}" + "Argument must be int, float, dict, iterable, " + f"or NodeStat/EdgeStat. Received {type(arg)}" ) elif isinstance(arg, dict): return {id: arg[id] for id in arg if id in ids} @@ -700,7 +724,8 @@ def _scalar_arg_to_dict(arg, ids, min_val, max_val): return {id: arg[idx] for idx, id in enumerate(ids)} else: raise TypeError( - f"Argument must be int, float, dict, iterable, or NodeStat/EdgeStat. Received {type(arg)}" + "Argument must be int, float, dict, iterable, " + f"or NodeStat/EdgeStat. Received {type(arg)}" ) @@ -730,7 +755,7 @@ def _color_arg_to_dict(arg, ids, cmap): return {id: arg[id] for id in arg if id in ids} elif isinstance(arg, str): return {id: arg for id in ids} - elif isinstance(arg, NodeStat) or isinstance(arg, EdgeStat): + elif isinstance(arg, (NodeStat, EdgeStat)): if isinstance(cmap, ListedColormap): vals = np.interp(arg.asnumpy(), [arg.min(), arg.max()], [0, cmap.N]) elif isinstance(cmap, LinearSegmentedColormap): @@ -743,7 +768,8 @@ def _color_arg_to_dict(arg, ids, cmap): return {id: arg[idx] for idx, id in enumerate(ids)} else: raise TypeError( - f"Argument must be str, dict, iterable, or NodeStat/EdgeStat. Received {type(arg)}" + "Argument must be str, dict, iterable, or " + f"NodeStat/EdgeStat. Received {type(arg)}" ) @@ -834,8 +860,8 @@ def draw_node_labels( zorder = max_edge_order(H) + 1 text_items = {} - for id, label in node_labels.items(): - (x, y) = pos[id] + for idx, label in node_labels.items(): + (x, y) = pos[idx] if not isinstance(label, str): label = str(label) @@ -856,7 +882,7 @@ def draw_node_labels( clip_on=clip_on_nodes, zorder=zorder, ) - text_items[id] = t + text_items[idx] = t return text_items @@ -885,8 +911,8 @@ def draw_hyperedge_labels( pos : dict Dictionary of positions node_id:(x,y). hyperedge_labels : bool or dict, optional - If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) pairs. - By default, False. + If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) + pairs. By default, False. font_size_edges : int, optional Font size for text labels, by default 10. font_color_edges : str, optional @@ -925,6 +951,7 @@ def draw_hyperedge_labels( draw_hyperedges draw_simplices draw_node_labels + """ if ax_edges is None: ax = plt.gca() @@ -1075,55 +1102,59 @@ def draw_hypergraph_hull( ---------- H : Hypergraph pos : dict, optional - If passed, this dictionary of positions node_id:(x,y) is used for placing the nodes. - If None (default), use the `barycenter_spring_layout` to compute the positions. + If passed, this dictionary of positions node_id:(x,y) is used for placing the + nodes. If None (default), use the `barycenter_spring_layout` to compute the + positions. ax : matplotlib.pyplot.axes, optional Axis to draw on. If None (default), get the current axes. dyad_color : str, dict, iterable, or EdgeStat, optional - Color of the dyadic links. If str, use the same color for all edges. If a dict, must - contain (edge_id: color_str) pairs. If iterable, assume the colors are - specified in the same order as the edges are found in H.edges. If EdgeStat, use a colormap - (specified with dyad_color_cmap) associated to it. By default, "black". + Color of the dyadic links. If str, use the same color for all edges. If a dict, + must contain (edge_id: color_str) pairs. If iterable, assume the colors are + specified in the same order as the edges are found in H.edges. If EdgeStat, use + a colormap (specified with dyad_color_cmap) associated to it. By default, + "black". edge_fc : str, dict, iterable, or EdgeStat, optional - Color of the hyperedges of order k>1. If str, use the same color for all hyperedges of order k>1. If a dict, must - contain (edge_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the hyperedges are found in H.edges. If EdgeStat, - use the colormap specified with edge_fc_cmap. If None (default), - use the H.edges.size. + Color of the hyperedges of order k>1. If str, use the same color for all + hyperedges of order k>1. If a dict, must contain (edge_id: color_str) pairs. + If other iterable, assume the colors are specified in the same order as the + hyperedges are found in H.edges. If EdgeStat, use the colormap specified with + edge_fc_cmap. If None (default), use the H.edges.size. edge_ec : str, dict, iterable, or EdgeStat, optional - Color of the borders of the hyperdges of order k>1. If str, use the same color for all edges. If a dict, must - contain (edge_id: color_str) pairs. If iterable, assume the colors are - specified in the same order as the edges are found in H.edges. If EdgeStat, use a colormap - (specified with edge_ec_cmap) associated to it. If None (default), - use the H.edges.size. + Color of the borders of the hyperdges of order k>1. If str, use the same color + for all edges. If a dict, must contain (edge_id: color_str) pairs. If iterable, + assume the colors are specified in the same order as the edges are found in + H.edges. If EdgeStat, use a colormap (specified with edge_ec_cmap) associated to + it. If None (default), use the H.edges.size. node_fc : node_fc : str, dict, iterable, or NodeStat, optional Color of the nodes. If str, use the same color for all nodes. If a dict, must contain (node_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the nodes are found in H.nodes. If NodeStat, - use the colormap specified with node_fc_cmap. By default, "tab:blue". + specified in the same order as the nodes are found in H.nodes. If NodeStat, use + the colormap specified with node_fc_cmap. By default, "tab:blue". node_ec : str, dict, iterable, or NodeStat, optional - Color of node borders. If str, use the same color for all nodes. If a dict, must - contain (node_id: color_str) pairs. If other iterable, assume the colors are - specified in the same order as the nodes are found in H.nodes. If NodeStat, + Color of node borders. If str, use the same color for all nodes. If a dict, + must contain (node_id: color_str) pairs. If other iterable, assume the colors + are specified in the same order as the nodes are found in H.nodes. If NodeStat, use the colormap specified with node_ec_cmap. By default, "black". node_lw : int, float, dict, iterable, or EdgeStat, optional - Line width of the node borders in pixels. If int or float, use the same width for all node borders. - If a dict, must contain (node_id: width) pairs. If iterable, assume the widths are - specified in the same order as the nodes are found in H.nodes. If NodeStat, use a monotonic - linear interpolation defined between min_node_lw and max_node_lw. By default, 1. + Line width of the node borders in pixels. If int or float, use the same width + for all node borders. If a dict, must contain (node_id: width) pairs. If + iterable, assume the widths are specified in the same order as the nodes are + found in H.nodes. If NodeStat, use a monotonic linear interpolation defined + between min_node_lw and max_node_lw. By default, 1. node_size : int, float, dict, iterable, or NodeStat, optional - Radius of the nodes in pixels. If int or float, use the same radius for all nodes. - If a dict, must contain (node_id: radius) pairs. If iterable, assume the radiuses are - specified in the same order as the nodes are found in H.nodes. If NodeStat, use a monotonic - linear interpolation defined between min_node_size and max_node_size. By default, 7. + Radius of the nodes in pixels. If int or float, use the same radius for all + nodes. If a dict, must contain (node_id: radius) pairs. If iterable, assume + the radiuses are specified in the same order as the nodes are found in + H.nodes. If NodeStat, use a monotonic linear interpolation defined between + min_node_size and max_node_size. By default, 7. max_order : int, optional Maximum of hyperedges to plot. If None (default), plots all orders. node_labels : bool, or dict, optional If True, draw ids on the nodes. If a dict, must contain (node_id: label) pairs. By default, False hyperedge_labels : bool, or dict, optional - If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) pairs. - By default, False. + If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) + pairs. By default, False. radius : float, optional Radius of the convex hull in the vicinity of the nodes, by default 0.05. **kwargs : optional args diff --git a/xgi/drawing/layout.py b/xgi/drawing/layout.py index d4f00724d..926c26fe9 100644 --- a/xgi/drawing/layout.py +++ b/xgi/drawing/layout.py @@ -19,10 +19,11 @@ def random_layout(H, center=None, dim=2, seed=None): - """Position nodes uniformly at random in the unit square. Exactly as networkx does. - For every node, a position is generated by choosing each of dim - coordinates uniformly at random on the interval [0.0, 1.0). - NumPy (http://scipy.org) is required for this function. + """Position nodes uniformly at random in the unit square. + + For every node, a position is generated by choosing each of dim coordinates + uniformly at random on the interval [0.0, 1.0). NumPy (http://scipy.org) is + required for this function. Parameters ---------- @@ -50,6 +51,10 @@ def random_layout(H, center=None, dim=2, seed=None): barycenter_spring_layout weighted_barycenter_spring_layout + Notes + ----- + This function proceeds exactly as NetworkX does. + Examples -------- >>> import xgi @@ -57,6 +62,7 @@ def random_layout(H, center=None, dim=2, seed=None): >>> ps = [0.1, 0.01] >>> H = xgi.random_hypergraph(N, ps) >>> pos = xgi.random_layout(H) + """ import numpy as np @@ -176,7 +182,8 @@ def barycenter_spring_layout(H, return_phantom_graph=False, seed=None): # Adding real nodes G.add_nodes_from(list(H.nodes)) - # Adding links (edges composed by two nodes only, for which we don't use phantom nodes + # Adding links (edges composed by two nodes only), + # for which we don't use phantom nodes for i, j in H.edges.filterby("order", 1).members(): G.add_edge(i, j) @@ -192,12 +199,14 @@ def barycenter_spring_layout(H, return_phantom_graph=False, seed=None): for d in range(2, max_edge_order(H) + 1): # Hyperedges of order d (d=2: triplets, etc.) for he in H.edges.filterby("order", d).members(): - # Adding one phantom node for each hyperedge and linking it to the nodes of the hyperedge + # Adding one phantom node for each hyperedge and linking it to the nodes of + # the hyperedge for n in he: G.add_edge(phantom_node_id, n) phantom_node_id += 1 - # Creating a dictionary for the position of the nodes with the standard spring layout + # Creating a dictionary for the position of the nodes with the standard spring + # layout pos_with_phantom_nodes = nx.spring_layout(G, seed=seed) # Retaining only the positions of the real nodes @@ -210,16 +219,15 @@ def barycenter_spring_layout(H, return_phantom_graph=False, seed=None): def weighted_barycenter_spring_layout(H, return_phantom_graph=False, seed=None): - """ - Position the nodes using Fruchterman-Reingold force-directed - algorithm using an augmented version of the the graph projection - of the hypergraph (or simplicial complex), where phantom nodes (barycenters) are created - for each edge of order d>1 (composed by more than two nodes). - Weights are assigned to all hyperedges of order 1 (links) and - to all connections to phantom nodes within each hyperedge - to keep them together. Weights scale as the order d. - If a simplicial complex is provided the results will be based on the - hypergraph constructed from its maximal simplices. + """Position the nodes using Fruchterman-Reingold force-directed algorithm. + + This uses an augmented version of the the graph projection of the hypergraph (or + simplicial complex), where phantom nodes (barycenters) are created for each edge of + order d>1 (composed by more than two nodes). Weights are assigned to all hyperedges + of order 1 (links) and to all connections to phantom nodes within each hyperedge to + keep them together. Weights scale as the order d. If a simplicial complex is + provided the results will be based on the hypergraph constructed from its maximal + simplices. Parameters ---------- @@ -249,6 +257,7 @@ def weighted_barycenter_spring_layout(H, return_phantom_graph=False, seed=None): >>> ps = [0.1, 0.01] >>> H = xgi.random_hypergraph(N, ps) >>> pos = xgi.weighted_barycenter_spring_layout(H) + """ if seed is not None: random.seed(seed) @@ -262,7 +271,8 @@ def weighted_barycenter_spring_layout(H, return_phantom_graph=False, seed=None): # Adding real nodes G.add_nodes_from(list(H.nodes)) - # Adding links (edges composed by two nodes only, for which we don't use phantom nodes) + # Adding links (edges composed by two nodes only), + # for which we don't use phantom nodes. d = 1 for i, j in H.edges.filterby("order", d).members(): G.add_edge(i, j, weight=d) @@ -279,12 +289,13 @@ def weighted_barycenter_spring_layout(H, return_phantom_graph=False, seed=None): for d in range(2, max_edge_order(H) + 1): # Hyperedges of order d (d=2: triplets, etc.) for he_id, members in H.edges.filterby("order", d).members(dtype=dict).items(): - # Adding one phantom node for each hyperedge and linking it to the nodes of the hyperedge + # Adding one phantom node for each hyperedge and linking it to the nodes of + # the hyperedge for n in members: G.add_edge(phantom_node_id, n, weight=d) phantom_node_id += 1 - # Creating a dictionary for the position of the nodes with the standard spring layout + # Creating a dictionary for node position with the standard spring layout pos_with_phantom_nodes = nx.spring_layout(G, weight="weight", seed=seed) # Retaining only the positions of the real nodes diff --git a/xgi/dynamics/synchronization.py b/xgi/dynamics/synchronization.py index 6c6349794..6d88f1981 100644 --- a/xgi/dynamics/synchronization.py +++ b/xgi/dynamics/synchronization.py @@ -29,10 +29,11 @@ def simulate_kuramoto(H, k2, k3, omega=None, theta=None, timesteps=10000, dt=0.0 k3 : float The coupling strength for triangles omega : numpy array of real values - The natural frequency of the nodes. If None (default), randomly drawn from a normal distribution + The natural frequency of the nodes. If None (default), randomly drawn from a + normal distribution theta : numpy array of real values - The initial phase distribution of nodes. If None (default), drawn from a random uniform distribution - on [0, 2pi[. + The initial phase distribution of nodes. If None (default), drawn from a random + uniform distribution on [0, 2pi[. timesteps : int greater than 1, default: 10000 The number of timesteps for Euler Method. dt : float greater than 0, default: 0.002 @@ -110,8 +111,9 @@ def simulate_kuramoto(H, k2, k3, omega=None, theta=None, timesteps=10000, dt=0.0 def compute_kuramoto_order_parameter(theta_time): - """This function calculates the order parameter for the Kuramoto model on hypergraphs, - from time series, which is a measure of synchrony. + """Calculate the order parameter for the Kuramoto model on hypergraphs. + + Calculation proceeds from time series, and the output is a measure of synchrony. Parameters ---------- @@ -142,9 +144,8 @@ def simulate_simplicial_kuramoto( n_steps=10000, index=False, ): - """ - This function simulates the simplicial Kuramoto model's dynamics on an oriented simplicial complex - using explicit Euler numerical integration scheme. + """Simulate the simplicial Kuramoto model's dynamics on an oriented simplicial + complex using explicit Euler numerical integration scheme. Parameters ---------- @@ -166,10 +167,10 @@ def simulate_simplicial_kuramoto( T: positive real value The final simulation time. n_steps: integer greater than 1 - The number of integration timesteps for - the explicit Euler method. + The number of integration timesteps for the explicit Euler method. index: bool, default: False - Specifies whether to output dictionaries mapping the node and edge IDs to indices + Specifies whether to output dictionaries mapping the node and edge IDs to + indices. Returns ------- @@ -205,7 +206,8 @@ def simulate_simplicial_kuramoto( if not isinstance(S, xgi.SimplicialComplex): raise XGIError( - "The simplicial Kuramoto model can be simulated only on a SimplicialComplex object" + "The simplicial Kuramoto model can be simulated " + "only on a SimplicialComplex object" ) if index: diff --git a/xgi/generators/classic.py b/xgi/generators/classic.py index e46743233..39a88d832 100644 --- a/xgi/generators/classic.py +++ b/xgi/generators/classic.py @@ -160,8 +160,7 @@ def trivial_hypergraph(n=1, create_using=None, default=None): def complete_hypergraph(N, order=None, max_order=None, include_singletons=False): - """ - Generate a complete hypergraph, i.e. one that contains all possible hyperdges + """Generate a complete hypergraph, i.e. one that contains all possible hyperdges at a given `order` or up to a `max_order`. Parameters @@ -170,9 +169,11 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) N : int Number of nodes order : int or None - If not None (default), specifies the single order for which to generate hyperedges + 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 + 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. @@ -188,10 +189,10 @@ def complete_hypergraph(N, order=None, max_order=None, include_singletons=False) Additionally, at least one of either must be specified. 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:`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. - + quickly becomes impractically long to compute, especially when using + `max_order`. For 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. """ # this import needs to happen when the function runs, not when the module is first diff --git a/xgi/generators/lattice.py b/xgi/generators/lattice.py index e34ff4dfe..235b3cdc5 100644 --- a/xgi/generators/lattice.py +++ b/xgi/generators/lattice.py @@ -43,8 +43,9 @@ def ring_lattice(n, d, k, l): Notes ----- - ring_lattice(n, 2, k, 0) is a ring lattice graph where each node has k//2 edges on either - side. + ring_lattice(n, 2, k, 0) is a ring lattice graph where each node has k//2 edges on + either side. + """ from ..classes import Hypergraph diff --git a/xgi/generators/random.py b/xgi/generators/random.py index e83786c12..dbfe46dad 100644 --- a/xgi/generators/random.py +++ b/xgi/generators/random.py @@ -252,8 +252,8 @@ def dcsbm_hypergraph(k1, k2, g1, g2, omega, seed=None): node_labels = [n for n, _ in sorted(k1.items(), key=lambda d: d[1], reverse=True)] edge_labels = [m for m, _ in sorted(k2.items(), key=lambda d: d[1], reverse=True)] - # these checks verify that the sum of node and edge degrees and the sum of node degrees - # and the sum of community connection matrix differ by less than a single edge. + # Verify that the sum of node and edge degrees and the sum of node degrees and the + # sum of community connection matrix differ by less than a single edge. if abs(sum(k1.values()) - sum(k2.values())) > 1: warnings.warn( "The sum of the degree sequence does not match the sum of the size sequence" @@ -261,7 +261,8 @@ def dcsbm_hypergraph(k1, k2, g1, g2, omega, seed=None): if abs(sum(k1.values()) - np.sum(omega)) > 1: warnings.warn( - "The sum of the degree sequence does not match the entries in the omega matrix" + "The sum of the degree sequence does not " + "match the entries in the omega matrix" ) # get indices for each community diff --git a/xgi/generators/simplicial_complexes.py b/xgi/generators/simplicial_complexes.py index 2da25f1d2..68bab8803 100644 --- a/xgi/generators/simplicial_complexes.py +++ b/xgi/generators/simplicial_complexes.py @@ -209,8 +209,10 @@ def flag_complex_d2(G, p2=None, seed=None): def random_flag_complex_d2(N, p, seed=None): - """Generate a maximal simplicial complex (up to order 2) from a - :math:`G_{N,p}` Erdős-Rényi random graph by filling all empty triangles with 2-simplices. + """Generate a maximal simplicial complex (up to order 2) from a :math:`G_{N,p}` + Erdős-Rényi random graph. + + This proceeds by filling all empty triangles in the graph with 2-simplices. Parameters ---------- @@ -229,6 +231,7 @@ def random_flag_complex_d2(N, p, seed=None): Notes ----- Computing all cliques quickly becomes heavy for large networks. + """ if seed is not None: random.seed(seed) @@ -243,7 +246,9 @@ def random_flag_complex_d2(N, p, seed=None): def random_flag_complex(N, p, max_order=2, seed=None): """Generate a flag (or clique) complex from a - :math:`G_{N,p}` Erdős-Rényi random graph by filling all cliques up to dimension max_order. + :math:`G_{N,p}` Erdős-Rényi random graph. + + This proceeds by filling all cliques up to dimension max_order. Parameters ---------- @@ -264,8 +269,8 @@ def random_flag_complex(N, p, max_order=2, seed=None): Notes ----- Computing all cliques quickly becomes heavy for large networks. - """ + """ if (p < 0) or (p > 1): raise ValueError("p must be between 0 and 1 included.") diff --git a/xgi/generators/uniform.py b/xgi/generators/uniform.py index 8dd4090af..c7df5da41 100644 --- a/xgi/generators/uniform.py +++ b/xgi/generators/uniform.py @@ -74,7 +74,8 @@ def uniform_hypergraph_configuration_model(k, m, seed=None): remainder = sum(k.values()) % m if remainder != 0: warnings.warn( - "This degree sequence is not realizable. Increasing the degree of random nodes so that it is." + "This degree sequence is not realizable. " + "Increasing the degree of random nodes so that it is." ) random_ids = random.sample(list(k.keys()), int(round(m - remainder))) for id in random_ids: @@ -365,11 +366,12 @@ def _index_to_edge_partition(index, partition_sizes, m): See Also -------- _index_to_edge + """ try: return [ int(index // np.prod(partition_sizes[r + 1 :]) % partition_sizes[r]) for r in range(m) ] - except: + except KeyError: raise Exception("Invalid parameters") diff --git a/xgi/linalg/hodge_matrix.py b/xgi/linalg/hodge_matrix.py index e0f5b2dfa..f3f62a862 100644 --- a/xgi/linalg/hodge_matrix.py +++ b/xgi/linalg/hodge_matrix.py @@ -52,9 +52,10 @@ def boundary_matrix(S, order=1, orientations=None, index=False): - """ - A function to generate the boundary matrices of an oriented simplicial complex. - The rows correspond to the (order-1)-simplices and the columns to the (order)-simplices. + """Generate the boundary matrices of an oriented simplicial complex. + + The rows correspond to the (order-1)-simplices and the columns to the + (order)-simplices. Parameters ---------- @@ -122,8 +123,8 @@ def boundary_matrix(S, order=1, orientations=None, index=False): key=lambda e: (isinstance(e, str), e) ) # Sort the simplex's vertices to get a reference orientation # The key is needed to sort a mixed list of numbers and strings: - # it ensures that node labels which are numbers are put before strings, - # thus giving a list [sorted numbers, sorted strings] + # it ensures that node labels which are numbers are put before + # strings, thus giving a list [sorted numbers, sorted strings] matrix_id = simplices_u_dict[u_simplex_id] head_idx = u_simplex[1] tail_idx = u_simplex[0] @@ -140,8 +141,8 @@ def boundary_matrix(S, order=1, orientations=None, index=False): key=lambda e: (isinstance(e, str), e) ) # Sort the simplex's vertices to get a reference orientation # The key is needed to sort a mixed list of numbers and strings: - # it ensures that node labels which are numbers are put before strings, - # thus giving a list [sorted numbers, sorted strings] + # it ensures that node labels which are numbers are put before + # strings, thus giving a list [sorted numbers, sorted strings] matrix_id = simplices_u_dict[u_simplex_id] u_simplex_subfaces = S._subfaces(u_simplex, all=False) subfaces_induced_orientation = [ diff --git a/xgi/linalg/hypergraph_matrix.py b/xgi/linalg/hypergraph_matrix.py index 0e1b479a7..e65847893 100644 --- a/xgi/linalg/hypergraph_matrix.py +++ b/xgi/linalg/hypergraph_matrix.py @@ -60,8 +60,7 @@ def incidence_matrix( H, order=None, sparse=True, index=False, weight=lambda node, edge, H: 1 ): - """ - A function to generate a weighted incidence matrix from a Hypergraph object, + """A function to generate a weighted incidence matrix from a Hypergraph object, where the rows correspond to nodes and the columns correspond to edges. Parameters @@ -74,7 +73,8 @@ def incidence_matrix( sparse: bool, default: True Specifies whether the output matrix is a scipy sparse matrix or a numpy matrix index: bool, default: False - Specifies whether to output dictionaries mapping the node and edge IDs to indices + Specifies whether to output dictionaries mapping the node and edge IDs to + indices. weight: lambda function, default=lambda function outputting 1 A function specifying the weight, given a node and edge @@ -179,7 +179,8 @@ def adjacency_matrix(H, order=None, sparse=True, s=1, weighted=False, index=Fals A.setdiag(0) if w: warn( - "Forming the adjacency matrix can be expensive when there are isolated nodes!" + "Forming the adjacency matrix can " + "be expensive when there are isolated nodes!" ) else: np.fill_diagonal(A, 0) @@ -219,8 +220,8 @@ def intersection_profile(H, order=None, sparse=True, index=False): return P """ - I, _, coldict = incidence_matrix(H, order=order, sparse=sparse, index=True) - P = I.T.dot(I) + eye, _, coldict = incidence_matrix(H, order=order, sparse=sparse, index=True) + P = eye.T.dot(eye) return (P, coldict) if index else P @@ -235,7 +236,8 @@ def degree_matrix(H, order=None, index=False): Order of interactions to use. If None (default), all orders are used. If int, must be >= 1. index: bool, default: False - Specifies whether to output disctionaries mapping the node and edge IDs to indices + Specifies whether to output disctionaries mapping the node and edge IDs to + indices. Returns ------- @@ -245,12 +247,12 @@ def degree_matrix(H, order=None, index=False): return K """ - I, rowdict, _ = incidence_matrix(H, order=order, index=True) + eye, rowdict, _ = incidence_matrix(H, order=order, index=True) - if I.shape == (0, 0): + if eye.shape == (0, 0): K = np.zeros(H.num_nodes) else: - K = np.ravel(np.sum(I, axis=1)) # flatten + K = np.ravel(np.sum(eye, axis=1)) # flatten return (K, rowdict) if index else K diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 2f1fa9c73..d91d41aab 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -67,11 +67,12 @@ def laplacian(H, order=1, sparse=False, rescale_per_node=False, index=False): Hypergraph order : int Order of interactions to consider. If order=1 (default), - returns the usual graph Laplacian + returns the usual graph Laplacian. sparse: bool, default: False - Specifies whether the output matrix is a scipy sparse matrix or a numpy matrix + Specifies whether the output matrix is a scipy sparse matrix or a numpy matrix. index: bool, default: False - Specifies whether to output disctionaries mapping the node and edge IDs to indices + Specifies whether to output disctionaries mapping the node and edge IDs to + indices. Returns ------- @@ -130,7 +131,8 @@ def multiorder_laplacian( rescale_per_node: bool, (default=False) Whether to rescale each Laplacian of order d by d (per node). index: bool, default: False - Specifies whether to output dictionaries mapping the node and edge IDs to indices + Specifies whether to output dictionaries mapping the node and edge IDs to + indices. Returns ------- @@ -169,7 +171,8 @@ def multiorder_laplacian( # avoid getting nans from dividing by 0 # manually setting contribution to 0 as it should be warn( - f"No edges of order {d}. Contribution of that order is zero. Its weight is effectively zero." + f"No edges of order {d}. Contribution of " + "that order is zero. Its weight is effectively zero." ) else: L_multi += L * w / np.mean(K) @@ -210,10 +213,8 @@ def normalized_hypergraph_laplacian(H, sparse=True, index=False): "Learning with Hypergraphs: Clustering, Classification, and Embedding" by Dengyong Zhou, Jiayuan Huang, Bernhard Schölkopf Advances in Neural Information Processing Systems (2006) - """ - - from ..algorithms import is_connected + """ if H.nodes.isolates(): raise XGIError( "Every node must be a member of an edge to avoid divide by zero error!" @@ -224,11 +225,11 @@ def normalized_hypergraph_laplacian(H, sparse=True, index=False): if sparse: Dinvsqrt = csr_array(diags(np.power(D, -0.5))) - I = csr_array((H.num_nodes, H.num_nodes)) - I.setdiag(1) + eye = csr_array((H.num_nodes, H.num_nodes)) + eye.setdiag(1) else: Dinvsqrt = np.diag(np.power(D, -0.5)) - I = np.eye(H.num_nodes) + eye = np.eye(H.num_nodes) - L = 0.5 * (I - Dinvsqrt @ A @ Dinvsqrt) + L = 0.5 * (eye - Dinvsqrt @ A @ Dinvsqrt) return (L, rowdict) if index else L diff --git a/xgi/readwrite/bipartite.py b/xgi/readwrite/bipartite.py index fdbb9e485..81ccd7bd8 100644 --- a/xgi/readwrite/bipartite.py +++ b/xgi/readwrite/bipartite.py @@ -196,7 +196,8 @@ def parse_bipartite_edgelist( node = nodetype(s[node_index]) except ValueError as e: raise TypeError( - f"Failed to convert the node with ID {s[node_index]} to type {nodetype}." + "Failed to convert the node with " + f"ID {s[node_index]} to type {nodetype}." ) from e else: node = s[node_index] @@ -207,7 +208,8 @@ def parse_bipartite_edgelist( edge = edgetype(s[edge_index]) except ValueError as e: raise TypeError( - f"Failed to convert the edge with ID {s[edge_index]} to type {edgetype}." + "Failed to convert the edge with " + f"ID {s[edge_index]} to type {edgetype}." ) from e else: edge = s[edge_index] diff --git a/xgi/readwrite/incidence.py b/xgi/readwrite/incidence.py index 72acc5c8f..d3cf747e8 100644 --- a/xgi/readwrite/incidence.py +++ b/xgi/readwrite/incidence.py @@ -78,5 +78,5 @@ def write_incidence_matrix(H, path, delimiter=" ", encoding="utf-8"): >>> # xgi.write_incidence_matrix(H, "test.csv", delimiter=",") """ - I = incidence_matrix(H, sparse=False) - np.savetxt(path, I, delimiter=delimiter, newline="\n", encoding=encoding) + eye = incidence_matrix(H, sparse=False) + np.savetxt(path, eye, delimiter=delimiter, newline="\n", encoding=encoding) diff --git a/xgi/readwrite/json.py b/xgi/readwrite/json.py index 5f526a369..f6eb97675 100644 --- a/xgi/readwrite/json.py +++ b/xgi/readwrite/json.py @@ -22,24 +22,24 @@ def write_json(H, path): # initialize empty data data = {} - # get overall hypergraph attributes, name always gets written (default is an empty string) - data["hypergraph-data"] = dict() + # name always gets written (default is an empty string) + data["hypergraph-data"] = {} data["hypergraph-data"].update(H._hypergraph) # get node data try: - data["node-data"] = {str(id): H.nodes[id] for id in H.nodes} - except: + data["node-data"] = {str(idx): H.nodes[idx] for idx in H.nodes} + except KeyError: raise XGIError("Node attributes not saved!") try: - data["edge-data"] = {str(id): H.edges[id] for id in H.edges} - except: + data["edge-data"] = {str(idx): H.edges[idx] for idx in H.edges} + except KeyError: raise XGIError("Edge attributes not saved!") # hyperedge dict data["edge-dict"] = { - str(id): [str(n) for n in H.edges.members(id)] for id in H.edges + str(idx): [str(n) for n in H.edges.members(idx)] for idx in H.edges } datastring = json.dumps(data, indent=2) diff --git a/xgi/readwrite/xgi_data.py b/xgi/readwrite/xgi_data.py index a99488817..e965df3ac 100644 --- a/xgi/readwrite/xgi_data.py +++ b/xgi/readwrite/xgi_data.py @@ -72,7 +72,9 @@ def load_xgi_data( ) else: warn( - f"No local copy was found at {cfp}. The data is requested from the xgi-data repository instead. To download a local copy, use `download_xgi_data`." + f"No local copy was found at {cfp}. The data is requested " + "from the xgi-data repository instead. To download a local " + "copy, use `download_xgi_data`." ) if cache: data = _request_from_xgi_data_cached(dataset) diff --git a/xgi/stats/__init__.py b/xgi/stats/__init__.py index 35491df79..925939680 100644 --- a/xgi/stats/__init__.py +++ b/xgi/stats/__init__.py @@ -44,9 +44,6 @@ """ -from collections import defaultdict -from typing import Callable - import numpy as np import pandas as pd from scipy.stats import moment as spmoment @@ -71,10 +68,10 @@ def __init__(self, network, view, func, args=None, kwargs=None): def __call__(self, *args, **kwargs): return self.__class__(self.net, self.view, self.func, args=args, kwargs=kwargs) - def __getitem__(self, id): - if id not in self.view: - raise IDNotFound(f'ID "{id}" not in this view') - return self.func(self.net, [id], *self.args, **self.kwargs)[id] + def __getitem__(self, idx): + if idx not in self.view: + raise IDNotFound(f'ID "{idx}" not in this view') + return self.func(self.net, [idx], *self.args, **self.kwargs)[idx] def __repr__(self): cls = self.__class__.__name__ @@ -355,7 +352,8 @@ def asnumpy(self): -------- >>> import xgi >>> H = xgi.Hypergraph([[1, 2, 3], [2, 3, 4, 5], [3, 4, 5]]) - >>> H.nodes.multi(['degree', 'clustering_coefficient']).asnumpy() # doctest: +NORMALIZE_WHITESPACE + >>> H.nodes.multi(['degree', 'clustering_coefficient']).asnumpy() + ... # doctest: +NORMALIZE_WHITESPACE array([[1. , 1. ], [2. , 0.66666667], [3. , 0.66666667], @@ -372,7 +370,8 @@ def aspandas(self): -------- >>> import xgi >>> H = xgi.Hypergraph([[1, 2, 3], [2, 3, 4, 5], [3, 4, 5]]) - >>> H.nodes.multi(['degree', 'clustering_coefficient']).aspandas() # doctest: +NORMALIZE_WHITESPACE + >>> H.nodes.multi(['degree', 'clustering_coefficient']).aspandas() + ... # doctest: +NORMALIZE_WHITESPACE degree clustering_coefficient 1 1 1.000000 2 2 0.666667 @@ -440,20 +439,21 @@ def dispatch_many_stats(kind, net, view, stats): def nodestat_func(func): - """Decorator that allows arbitrary functions to behave like :class:`NodeStat` objects. + """Decorate arbitrary functions to behave like :class:`NodeStat` objects. Parameters ---------- func : callable Function or callable with signature `func(net, bunch)`, where `net` is the network and `bunch` is an iterable of nodes in `net`. The call `func(net, - bunch)` must return a dict with pairs of the form `(node: value)` where `node` is - in `bunch` and `value` is the value of the statistic at `node`. + bunch)` must return a dict with pairs of the form `(node: value)` where `node` + is in `bunch` and `value` is the value of the statistic at `node`. Returns ------- callable - The decorated callable unmodified, after registering it in the `stats` framework. + The decorated callable unmodified, after registering it in the `stats` + framework. See Also -------- @@ -534,7 +534,7 @@ def nodestat_func(func): def edgestat_func(func): - """Decorator that allows arbitrary functions to behave like :class:`EdgeStat` objects. + """Decorate arbitrary functions to behave like :class:`EdgeStat` objects. Works identically to :func:`nodestat`. For extended documentation, see :func:`nodestat_func`. @@ -550,7 +550,8 @@ def edgestat_func(func): Returns ------- callable - The decorated callable unmodified, after registering it in the `stats` framework. + The decorated callable unmodified, after registering it in the `stats` + framework. See Also -------- diff --git a/xgi/stats/nodestats.py b/xgi/stats/nodestats.py index da072745b..e7d59e701 100644 --- a/xgi/stats/nodestats.py +++ b/xgi/stats/nodestats.py @@ -251,8 +251,8 @@ def local_clustering_coefficient(net, bunch): References ---------- - "Properties of metabolic graphs: biological organization or representation artifacts?" - by Wanding Zhou and Luay Nakhleh. + "Properties of metabolic graphs: biological organization or representation + artifacts?" by Wanding Zhou and Luay Nakhleh. https://doi.org/10.1186/1471-2105-12-132 "Hypergraphs for predicting essential genes using multiprotein complex data" @@ -265,6 +265,7 @@ def local_clustering_coefficient(net, bunch): >>> H = xgi.random_hypergraph(3, [1, 1]) >>> H.nodes.local_clustering_coefficient.asdict() {0: 1.0, 1: 1.0, 2: 1.0} + """ cc = xgi.local_clustering_coefficient(net) return {n: cc[n] for n in cc if n in bunch}