diff --git a/docs/source/api/classes/xgi.classes.hypergraph.Hypergraph.rst b/docs/source/api/classes/xgi.classes.hypergraph.Hypergraph.rst index 28d4042ea..508d984ff 100644 --- a/docs/source/api/classes/xgi.classes.hypergraph.Hypergraph.rst +++ b/docs/source/api/classes/xgi.classes.hypergraph.Hypergraph.rst @@ -36,8 +36,6 @@ ~Hypergraph.remove_nodes_from ~Hypergraph.remove_edges_from ~Hypergraph.remove_node_from_edge - ~Hypergraph.remove_isolates - ~Hypergraph.remove_singleton_edges ~Hypergraph.clear ~Hypergraph.clear_edges @@ -56,7 +54,5 @@ .. autosummary:: :nosignatures: - ~Hypergraph.isolates ~Hypergraph.duplicate_edges ~Hypergraph.has_edge - ~Hypergraph.singleton_edges diff --git a/docs/source/api/classes/xgi.classes.reportviews.EdgeView.rst b/docs/source/api/classes/xgi.classes.reportviews.EdgeView.rst index a1e590c2a..0b227057e 100644 --- a/docs/source/api/classes/xgi.classes.reportviews.EdgeView.rst +++ b/docs/source/api/classes/xgi.classes.reportviews.EdgeView.rst @@ -21,6 +21,7 @@ xgi.classes.reportviews.EdgeView :nosignatures: ~EdgeView.members + ~EdgeView.singletons ~IDView.neighbors ~IDView.filterby ~IDView.filterby_attr diff --git a/docs/source/api/classes/xgi.classes.reportviews.NodeView.rst b/docs/source/api/classes/xgi.classes.reportviews.NodeView.rst index 0b6614a61..a145a7da1 100644 --- a/docs/source/api/classes/xgi.classes.reportviews.NodeView.rst +++ b/docs/source/api/classes/xgi.classes.reportviews.NodeView.rst @@ -21,6 +21,7 @@ xgi.classes.reportviews.NodeView :nosignatures: ~NodeView.memberships + ~NodeView.isolates ~IDView.neighbors ~IDView.filterby ~IDView.filterby_attr diff --git a/docs/source/api/readwrite/xgi.readwrite.xgi_data.rst b/docs/source/api/readwrite/xgi.readwrite.xgi_data.rst index 39b837b5d..df22f1de0 100644 --- a/docs/source/api/readwrite/xgi.readwrite.xgi_data.rst +++ b/docs/source/api/readwrite/xgi.readwrite.xgi_data.rst @@ -1,10 +1,10 @@ xgi.readwrite.xgi_data -================== +====================== .. currentmodule:: xgi.readwrite.xgi_data .. automodule:: xgi.readwrite.xgi_data - + .. rubric:: Functions - - .. autofunction:: load_xgi_data \ No newline at end of file + + .. autofunction:: load_xgi_data diff --git a/tests/classes/test_hypergraph.py b/tests/classes/test_hypergraph.py index 1bb2cf866..664293ca4 100644 --- a/tests/classes/test_hypergraph.py +++ b/tests/classes/test_hypergraph.py @@ -100,32 +100,15 @@ def test_add_nodes_from(attr1, attr2, attr3): assert H.nodes[2]["color"] == attr3["color"] -def test_singleton_edges(edgelist1, edgelist2): - H1 = xgi.Hypergraph(edgelist1) - H2 = xgi.Hypergraph(edgelist2) - - assert len(H1.singleton_edges()) == 1 - assert 1 in H1.singleton_edges() - assert len(H2.singleton_edges()) == 0 - - def test_remove_singleton_edges(edgelist1, edgelist2): H1 = xgi.Hypergraph(edgelist1) H2 = xgi.Hypergraph(edgelist2) - H1.remove_singleton_edges() - H2.remove_singleton_edges() - - assert H1.singleton_edges() == {} - assert H2.singleton_edges() == {} + H1.remove_edges_from(H1.edges.singletons()) + H2.remove_edges_from(H2.edges.singletons()) - -def test_isolates(edgelist1): - H = xgi.Hypergraph(edgelist1) - assert H.isolates(ignore_singletons=False) == set() - assert H.isolates() == {4} - H.remove_isolates() - assert 4 not in H + assert list(H1.edges.singletons()) == [] + assert list(H2.edges.singletons()) == [] def test_add_node_attr(edgelist1): diff --git a/tests/classes/test_hypergraphviews.py b/tests/classes/test_hypergraphviews.py index 85d441f02..1fb73d88f 100644 --- a/tests/classes/test_hypergraphviews.py +++ b/tests/classes/test_hypergraphviews.py @@ -52,14 +52,14 @@ def test_subhypergraph(edgelist1): new_H = xgi.classes.hypergraphviews.subhypergraph(H, edges=[1, 2]) assert list(new_H.nodes) == [1, 2, 3, 4, 5, 6, 7, 8] assert list(new_H.edges) == [1, 2] - assert new_H.isolates(ignore_singletons=False) == {1, 2, 3, 7, 8} + assert list(new_H.nodes.isolates(ignore_singletons=False)) == [1, 2, 3, 7, 8] new_H = xgi.classes.hypergraphviews.subhypergraph( H, nodes=[1, 2, 3, 4, 5], edges=[1, 2] ) assert list(new_H.nodes) == [1, 2, 3, 4, 5] assert list(new_H.edges) == [1] - assert new_H.isolates(ignore_singletons=False) == {1, 2, 3, 5} + assert list(new_H.nodes.isolates(ignore_singletons=False)) == [1, 2, 3, 5] new_H = xgi.classes.hypergraphviews.subhypergraph(H, nodes=[4], edges=[0, 1, 2]) assert list(new_H.nodes) == [4] @@ -68,4 +68,4 @@ def test_subhypergraph(edgelist1): new_H = xgi.classes.hypergraphviews.subhypergraph(H, nodes=[3, 4, 5, 6], edges=[2]) assert list(new_H.nodes) == [3, 4, 5, 6] assert list(new_H.edges) == [2] - assert new_H.isolates(ignore_singletons=False) == {3, 4} + assert list(new_H.nodes.isolates(ignore_singletons=False)) == [3, 4] diff --git a/tests/classes/test_reportviews.py b/tests/classes/test_reportviews.py index 21e01f376..82b772f9a 100644 --- a/tests/classes/test_reportviews.py +++ b/tests/classes/test_reportviews.py @@ -205,3 +205,19 @@ def test_call(edgelist1): assert len(H.nodes([])) == 0 assert H.nodes(list(H.nodes)) == H.nodes assert H.nodes(H.nodes) == H.nodes + + +def test_isolates(edgelist1): + H = xgi.Hypergraph(edgelist1) + assert list(H.nodes.isolates(ignore_singletons=False)) == [] + assert list(H.nodes.isolates()) == [4] + H.remove_nodes_from(H.nodes.isolates()) + assert 4 not in H + + +def test_singletons(edgelist1): + H = xgi.Hypergraph(edgelist1) + assert list(H.edges.singletons()) == [1] + H.remove_edge(1) + assert 1 not in H.edges + assert list(H.edges.singletons()) == [] diff --git a/xgi/classes/hypergraph.py b/xgi/classes/hypergraph.py index dd5820dc1..e5d40de49 100644 --- a/xgi/classes/hypergraph.py +++ b/xgi/classes/hypergraph.py @@ -978,73 +978,6 @@ def dual(self): return dual - def singleton_edges(self): - """Edges with a single member. - - Returns - ------- - EdgeView - View of the edges with a single member. - - """ - return self.edges.filterby("order", 0) - - def remove_singleton_edges(self): - """Removes all singletons edges from the hypergraph""" - singleton_ids = [ - id_ for id_, members in self._edge.items() if len(members) == 1 - ] - self.remove_edges_from(singleton_ids) - - def isolates(self, ignore_singletons=True): - """Nodes that belong to no edges. - - When ignore_singletons is True (default), a node is considered isolated frmo 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, a node is isolated only when it is a member of - exactly zero edges, including singletons. - - Parameters - ---------- - ignore_singletons : bool, default False - Whether to consider singleton edges. - - Returns - ------- - set - Isolated nodes. - - See Also - -------- - remove_isolates - - """ - nodes_in_edges = set() - for idx in self.edges: - edge = self.edges.members(idx) - if ignore_singletons and len(edge) == 1: - continue - nodes_in_edges = nodes_in_edges.union(edge) - return set(self.nodes) - nodes_in_edges - - def remove_isolates(self, ignore_singletons=True): - """Remove all nodes that belong to no edges. - - Parameters - ---------- - ignore_singletons : bool, default False - Whether to consider singleton edges when searching for isolated nodes. - - See Also - -------- - isolates - - """ - self.remove_nodes_from(self.isolates(ignore_singletons)) - def duplicate_edges(self): """A list of all duplicate edges. @@ -1053,10 +986,6 @@ def duplicate_edges(self): list All edges with a duplicate. - See also - -------- - remove_duplicates - """ edges = [tuple(e) for e in self._edge.values()] edges_unique, counts = np.unique(edges, return_counts=True) diff --git a/xgi/classes/reportviews.py b/xgi/classes/reportviews.py index 468f41a09..15674caec 100644 --- a/xgi/classes/reportviews.py +++ b/xgi/classes/reportviews.py @@ -445,6 +445,42 @@ def memberships(self, n=None): """ return self._id_dict.copy() if n is None else self._id_dict[n].copy() + def isolates(self, ignore_singletons=True): + """Nodes that belong to no edges. + + When ignore_singletons is True (default), 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, a node is isolated only when it is a member of + exactly zero edges, including singletons. + + Parameters + ---------- + ignore_singletons : bool, default False + Whether to consider singleton edges. + + Returns + ------- + NodeView containing the isolated nodes. + + See Also + -------- + :meth:`EdgeView.singletons` + + """ + if ignore_singletons: + nodes_in_edges = set() + for members in self._bi_id_dict.values(): + if len(members) == 1: + continue + nodes_in_edges = nodes_in_edges.union(members) + isolates = set(self._id_dict) - nodes_in_edges + return self.from_view(self, bunch=isolates) + else: + return self.filterby("degree", 0) + class EdgeView(IDView): """An IDView that keeps track of edge ids. @@ -513,3 +549,17 @@ def members(self, e=None, dtype=list): raise IDNotFound(f'ID "{e}" not in this view') return self._id_dict[e].copy() + + def singletons(self): + """Edges that contain exactly one node. + + Returns + ------- + EdgeView containing the singleton edges. + + See Also + -------- + :meth:`NodeView.isolates` + + """ + return self.filterby("size", 1)