From 9f32d2cd772e9f78d04c5c161c0b26a520cc2842 Mon Sep 17 00:00:00 2001 From: Maxime Lucas Date: Fri, 9 Dec 2022 15:05:57 +0100 Subject: [PATCH] Non recursive add simplex (#247) * FIX: rewrote add_simplices_from to make it non-recursive. tests: fixed some * fix: some tests * fix: test * fix: put the docstring back in * non-recursive add_simplex * fix: remove supfaces of edge #85 * style: ran black * nich request * fix: id and review comment * refactor: added helper function to add simplices without checks, for clarity and to avoid redundancy * fix: changed tests to check sets to satistfy different python versions * fix: boundary_matrix test - thanks Marco * fix: more tests * fix: more tests * tests: skip random doctests * fix: infinite loop. tests: reinstated warning tests * run isort and black * remove unnecessary import Co-authored-by: Nicholas Landry --- tests/classes/test_hypergraph.py | 1 - tests/classes/test_simplicialcomplex.py | 203 +++++++++++++--------- tests/generators/test_classic.py | 4 +- tests/linalg/test_matrix.py | 112 +++++++++++-- xgi/classes/simplicialcomplex.py | 213 ++++++++++++++++-------- xgi/utils/utilities.py | 2 +- 6 files changed, 367 insertions(+), 168 deletions(-) diff --git a/tests/classes/test_hypergraph.py b/tests/classes/test_hypergraph.py index ee1ddbcaf..2eecc0a73 100644 --- a/tests/classes/test_hypergraph.py +++ b/tests/classes/test_hypergraph.py @@ -1,6 +1,5 @@ import pickle import tempfile -from warnings import warn import pytest diff --git a/tests/classes/test_simplicialcomplex.py b/tests/classes/test_simplicialcomplex.py index d5d8a2b4c..1a932d0cb 100644 --- a/tests/classes/test_simplicialcomplex.py +++ b/tests/classes/test_simplicialcomplex.py @@ -1,6 +1,7 @@ -import pytest from warnings import warn +import pytest + import xgi from xgi.exception import XGIError @@ -38,18 +39,19 @@ def test_add_simplex(): S = xgi.SimplicialComplex() S.add_simplex([1, 2, 3]) - edge_dict = { - 0: frozenset({1, 2, 3}), - 1: frozenset({1, 2}), - 2: frozenset({1, 3}), - 3: frozenset({2, 3}), - } + edges = [ + frozenset({1, 2, 3}), + frozenset({2, 3}), + frozenset({1, 2}), + frozenset({1, 3}), + ] assert S.num_nodes == 3 - assert S._edge == edge_dict + assert list(S.edges) == list(range(4)) + assert set(S.edges.members()) == set(edges) S.add_simplex([2, 1]) - assert S._edge == edge_dict + assert set(S.edges.members()) == set(edges) # check uid S2 = xgi.SimplicialComplex() @@ -84,28 +86,22 @@ def test_add_simplices_from_iterable_of_members(): ] S = xgi.SimplicialComplex() S.add_simplices_from(edges) - assert S.edges.members() == simplices1 + assert set(S.edges.members()) == set(simplices1) S1 = xgi.SimplicialComplex(edges) with pytest.raises(XGIError): xgi.SimplicialComplex(S1.edges) edges = {frozenset([0, 1]), frozenset([1, 2]), frozenset([1, 2, 4])} - simplices2 = [ - frozenset({0, 1}), - frozenset({1, 2, 4}), - frozenset({1, 2}), - frozenset({1, 4}), - frozenset({2, 4}), - ] + S = xgi.SimplicialComplex() S.add_simplices_from(edges) - assert S.edges.members() == simplices2 + assert set(S.edges.members()) == set(simplices1) edges = [[0, 1], {1, 2}, (1, 2, 4)] S = xgi.SimplicialComplex() S.add_simplices_from(edges) - assert S.edges.members() == simplices1 + assert set(S.edges.members()) == set(simplices1) edges = [{"foo", "bar"}, {"bar", "baz"}, {"foo", "bar", "baz"}] simplices3 = [ @@ -137,40 +133,62 @@ def test_add_simplices_from_iterable_of_members(): def test_add_simplices_from_format2(): edges = [({0, 1}, 0), ({1, 2}, 1), ({1, 2, 4}, 2)] - simplices1 = [ - frozenset({0, 1}), - frozenset({1, 2}), - frozenset({1, 2, 4}), - frozenset({1, 4}), - frozenset({2, 4}), - ] + simplices1 = { + 0: frozenset({0, 1}), + 1: frozenset({1, 2}), + 2: frozenset({1, 2, 4}), + 3: frozenset({2, 4}), + 4: frozenset({1, 4}), + } H = xgi.SimplicialComplex() H.add_simplices_from(edges) - assert list(H.edges) == list(range(6)) - assert H.edges.members(dtype=dict) == simplices1 + assert list(H.edges) == list(range(5)) + assert H._edge == simplices1 edges = [({0, 1}, "a"), ({1, 2}, "b"), ({1, 2, 4}, "foo")] + simplices = { + "a": frozenset({0, 1}), + "b": frozenset({1, 2}), + "foo": frozenset({1, 2, 4}), + 0: frozenset({2, 4}), + 1: frozenset({1, 4}), + } H = xgi.SimplicialComplex() H.add_simplices_from(edges) - assert list(H.edges) == list(range(6)) - assert H.edges.members(dtype=dict) == {e[1]: e[0] for e in edges} + assert list(H.edges) == ["a", "b", "foo", 0, 1] + assert H._edge == simplices edges = [({0, 1}, "a"), ({1, 2}, "b"), ({2, 3, 4}, 100)] + simplices = [ + frozenset({0, 1}), + frozenset({1, 2}), + frozenset({2, 3, 4}), + frozenset({2, 3}), + frozenset({2, 4}), + frozenset({3, 4}), + ] H = xgi.SimplicialComplex() H.add_simplices_from(edges) - assert list(H.edges) == [e[1] for e in edges] - assert H.edges.members(dtype=dict) == {e[1]: e[0] for e in simplices1} + assert list(H.edges) == ["a", "b", 100, 101, 102, 103] + assert set(H.edges.members()) == set(simplices) # check counter - H.add_edge([1, 9, 2]) - assert H.edges.members(101) == {1, 9, 2} + H.add_simplex([1, 9, 2]) + assert next(H._edge_uid) == 107 H1 = xgi.SimplicialComplex([{1, 2}, {2, 3, 4}]) with pytest.warns( - UserWarning, match="uid 0 already exists, cannot add edge {1, 3}." + UserWarning, match="uid 0 already exists, cannot add simplex {1, 3}." ): - H1.add_edges_from([({1, 3}, 0)]) - assert H1._edge == {0: {1, 2}, 1: {2, 3, 4}} + H1.add_simplices_from([({1, 3}, 0)]) + simplices = [ + frozenset({1, 2}), + frozenset({2, 3, 4}), + frozenset({2, 3}), + frozenset({2, 4}), + frozenset({3, 4}), + ] + assert set(H1.edges.members()) == set(simplices) def test_add_simplices_from_format3(): @@ -179,17 +197,17 @@ def test_add_simplices_from_format3(): ({1, 2}, {"age": 30}), ({1, 2, 4}, {"color": "blue", "age": 40}), ] - simplices1 = [ - frozenset({0, 1}), - frozenset({1, 2}), - frozenset({1, 2, 4}), - frozenset({1, 4}), - frozenset({2, 4}), - ] + simplices1 = { + 0: frozenset({0, 1}), + 1: frozenset({1, 2}), + 2: frozenset({1, 2, 4}), + 3: frozenset({2, 4}), + 4: frozenset({1, 4}), + } H = xgi.SimplicialComplex() H.add_simplices_from(edges) assert list(H.edges) == list(range(5)) - assert H.edges.members() == simplices1 + assert H._edge == simplices1 assert H.edges[0] == edges[0][1] assert H.edges[1] == edges[1][1] assert H.edges[2] == edges[2][1] @@ -197,7 +215,7 @@ def test_add_simplices_from_format3(): assert H.edges[4] == dict() # check counter H.add_simplex([1, 9, 2]) - assert H.edges.members(5) == {1, 9, 2} + assert next(H._edge_uid) == 8 def test_add_simplices_from_format4(): @@ -206,18 +224,18 @@ def test_add_simplices_from_format4(): ({1, 2}, "two", {"age": 30}), ({1, 2, 4}, "three", {"color": "blue", "age": 40}), ] - simplices1 = [ - frozenset({0, 1}), - frozenset({1, 2}), - frozenset({1, 2, 4}), - frozenset({1, 4}), - frozenset({2, 4}), - ] + simplices1 = { + "one": frozenset({0, 1}), + "two": frozenset({1, 2}), + "three": frozenset({1, 2, 4}), + 0: frozenset({2, 4}), + 1: frozenset({1, 4}), + } H = xgi.SimplicialComplex() H.add_simplices_from(edges) assert list(H.edges) == ["one", "two", "three", 0, 1] - assert H.edges.members() == simplices1 + assert H._edge == simplices1 assert H.edges["one"] == edges[0][2] assert H.edges["two"] == edges[1][2] assert H.edges["three"] == edges[2][2] @@ -225,7 +243,7 @@ def test_add_simplices_from_format4(): assert H.edges[1] == dict() # check counter H.add_simplex([1, 9, 2]) - assert H.edges.members(2) == {1, 9, 2} + assert next(H._edge_uid) == 5 H1 = xgi.SimplicialComplex([{1, 2}, {2, 3, 4}]) with pytest.warns( @@ -237,19 +255,20 @@ def test_add_simplices_from_format4(): def test_add_edges_from_dict(): edges = {"one": [0, 1], "two": [1, 2], 2: [1, 2, 4]} - simplices1 = [ - frozenset({0, 1}), - frozenset({1, 2}), - frozenset({1, 2, 4}), - frozenset({1, 4}), - frozenset({2, 4}), - ] + simplices1 = { + "one": frozenset({0, 1}), + "two": frozenset({1, 2}), + 2: frozenset({1, 2, 4}), + 3: frozenset({2, 4}), + 4: frozenset({1, 4}), + } + H = xgi.SimplicialComplex() H.add_simplices_from(edges) assert list(H.edges) == ["one", "two", 2, 3, 4] - assert H.edges.members() == simplices1 + assert H._edge == simplices1 # check counter - H.add_edge([1, 9, 2]) + H.add_simplex([1, 9, 2]) assert H.edges.members(5) == {1, 9, 2} H1 = xgi.SimplicialComplex([{1, 2}, {2, 3, 4}]) @@ -281,12 +300,14 @@ def test_add_simplices_from(edgelist5): simplex = ((1, 2, 3), {"color": "red"}) S3.add_simplices_from([simplex], max_order=2) - assert S3.edges.members(dtype=dict) == { - 0: frozenset({1, 2, 3}), - 1: frozenset({1, 2}), - 2: frozenset({1, 3}), - 3: frozenset({2, 3}), - } + simplices = [ + frozenset({1, 2, 3}), + frozenset({2, 3}), + frozenset({1, 2}), + frozenset({1, 3}), + ] + + assert set(S3.edges.members()) == set(simplices) assert S3.edges[0] == {"color": "red"} assert S3.edges[1] == {} @@ -420,14 +441,34 @@ def test_remove_simplex_id(edgelist6): S.add_simplices_from(edgelist6) # remove simplex and others it belongs to - S.remove_simplex_id(6) # simplex {2, 3} - edge_dict = { - 0: frozenset({0, 1, 2}), - 1: frozenset({0, 1}), - 2: frozenset({0, 2}), - 3: frozenset({1, 2}), - 5: frozenset({1, 3}), - 8: frozenset({2, 4}), - 9: frozenset({3, 4}), - } - assert S._edge == edge_dict + id = list(S._edge.values()).index(frozenset({2, 3})) + S.remove_simplex_id(id) # simplex {2, 3} + edges = [ + frozenset({0, 1, 2}), + frozenset({0, 1}), + frozenset({2, 4}), + frozenset({1, 2}), + frozenset({3, 4}), + frozenset({0, 2}), + frozenset({1, 3}), + ] + assert set(S.edges.members()) == set(edges) + + +def test_remove_simplex_ids_from(edgelist6): + S = xgi.SimplicialComplex() + S.add_simplices_from(edgelist6) + + # remove simplex and others it belongs to + id1 = list(S._edge.values()).index(frozenset({2, 3})) + id2 = list(S._edge.values()).index(frozenset({0, 1, 2})) + S.remove_simplex_ids_from([id1, id2]) + edges = [ + frozenset({0, 1}), + frozenset({2, 4}), + frozenset({1, 2}), + frozenset({3, 4}), + frozenset({0, 2}), + frozenset({1, 3}), + ] + assert set(S.edges.members()) == set(edges) diff --git a/tests/generators/test_classic.py b/tests/generators/test_classic.py index 43933dbf1..eb6ef3dbe 100644 --- a/tests/generators/test_classic.py +++ b/tests/generators/test_classic.py @@ -59,7 +59,9 @@ def test_flag_complex(): G1 = nx.complete_graph(4) S4 = xgi.flag_complex(G1) S5 = xgi.flag_complex(G1, ps=[1]) - assert S4.edges.members() == S5.edges.members() + assert S4.num_nodes == S5.num_nodes + assert S4.num_edges == S5.num_edges + assert set(S4.edges.members()) == set(S5.edges.members()) def test_ring_lattice(): diff --git a/tests/linalg/test_matrix.py b/tests/linalg/test_matrix.py index 5a0efa5f4..c64f26b40 100644 --- a/tests/linalg/test_matrix.py +++ b/tests/linalg/test_matrix.py @@ -440,23 +440,115 @@ def test_boundary_matrix(edgelist4): assert facedict1 == facedict2 assert tetdict1 == tetdict2 - assert np.linalg.norm(B1[0:2, 0] - [-1, 1]) == 0 - assert np.linalg.norm(B2[0:3, 0] - [1, -1, 1]) == 0 - assert np.linalg.norm(B3[0:5, 0] - [0, -1, 1, -1, 1]) == 0 - assert np.linalg.norm(np.sum(B1, 0) - [0, 0, 0, 0, 0, 0, 0, 0]) == 0 + nodedict1 = {k: v for v, k in nodedict1.items()} + edgedict1 = {k: v for v, k in edgedict1.items()} + facedict1 = {k: v for v, k in facedict1.items()} + tetdict1 = {k: v for v, k in tetdict1.items()} + + iddict = S1.edges.ids + iddict = {simplex: v for v, simplex in iddict.items()} + + i123 = facedict1[iddict[frozenset([1, 2, 3])]] + i2345 = tetdict1[iddict[frozenset([2, 3, 4, 5])]] + i345 = facedict1[iddict[frozenset([3, 4, 5])]] + i245 = facedict1[iddict[frozenset([2, 4, 5])]] + i12 = edgedict1[iddict[frozenset([1, 2])]] + i24 = edgedict1[iddict[frozenset([2, 4])]] + i34 = edgedict1[iddict[frozenset([3, 4])]] + i235 = facedict1[iddict[frozenset([2, 3, 5])]] + i23 = edgedict1[iddict[frozenset([2, 3])]] + i45 = edgedict1[iddict[frozenset([4, 5])]] + i234 = facedict1[iddict[frozenset([2, 3, 4])]] + i25 = edgedict1[iddict[frozenset([2, 5])]] + i13 = edgedict1[iddict[frozenset([1, 3])]] + i35 = edgedict1[iddict[frozenset([3, 5])]] + + assert B1[nodedict1[1], i12] == -1 + assert B1[nodedict1[2], i12] == 1 + assert B1[nodedict1[2], i24] == -1 + assert B1[nodedict1[4], i24] == 1 + assert B1[nodedict1[3], i34] == -1 + assert B1[nodedict1[4], i34] == 1 + assert B1[nodedict1[2], i23] == -1 + assert B1[nodedict1[3], i23] == 1 + assert B1[nodedict1[4], i45] == -1 + assert B1[nodedict1[5], i45] == 1 + assert B1[nodedict1[2], i25] == -1 + assert B1[nodedict1[5], i25] == 1 + assert B1[nodedict1[1], i13] == -1 + assert B1[nodedict1[3], i13] == 1 + assert B1[nodedict1[3], i35] == -1 + assert B1[nodedict1[5], i35] == 1 + + assert B2[i12, i123] == 1 + assert B2[i23, i123] == 1 + assert B2[i13, i123] == -1 + assert B2[i34, i345] == 1 + assert B2[i45, i345] == 1 + assert B2[i35, i345] == -1 + assert B2[i24, i245] == 1 + assert B2[i45, i245] == 1 + assert B2[i25, i245] == -1 + assert B2[i23, i235] == 1 + assert B2[i35, i235] == 1 + assert B2[i25, i235] == -1 + assert B2[i23, i234] == 1 + assert B2[i34, i234] == 1 + assert B2[i24, i234] == -1 + + assert B3[i345, i2345] == 1 + assert B3[i245, i2345] == -1 + assert B3[i235, i2345] == 1 + assert B3[i234, i2345] == -1 + assert np.linalg.norm(B1 @ B2) == 0 assert np.linalg.norm(B2 @ B3) == 0 - # Change the orientation of a face and an edge - orientations[0] = 1 - orientations[1] = 1 + # Change the orientation of a face + orientations[iddict[frozenset([3, 4, 5])]] = 1 B1 = xgi.boundary_matrix(S1, order=1, orientations=orientations, index=False) B2 = xgi.boundary_matrix(S1, order=2, orientations=orientations, index=False) B3 = xgi.boundary_matrix(S1, order=3, orientations=orientations, index=False) - assert np.linalg.norm(B1[0:2, 0] - [1, -1]) == 0 - assert np.linalg.norm(B2[0:3, 0] - [1, 1, -1]) == 0 - assert np.linalg.norm(B3[0:5, 0] - [0, -1, 1, -1, 1]) == 0 + + assert B1[nodedict1[1], i12] == -1 + assert B1[nodedict1[2], i12] == 1 + assert B1[nodedict1[2], i24] == -1 + assert B1[nodedict1[4], i24] == 1 + assert B1[nodedict1[3], i34] == -1 + assert B1[nodedict1[4], i34] == 1 + assert B1[nodedict1[2], i23] == -1 + assert B1[nodedict1[3], i23] == 1 + assert B1[nodedict1[4], i45] == -1 + assert B1[nodedict1[5], i45] == 1 + assert B1[nodedict1[2], i25] == -1 + assert B1[nodedict1[5], i25] == 1 + assert B1[nodedict1[1], i13] == -1 + assert B1[nodedict1[3], i13] == 1 + assert B1[nodedict1[3], i35] == -1 + assert B1[nodedict1[5], i35] == 1 + + assert B2[i12, i123] == 1 + assert B2[i23, i123] == 1 + assert B2[i13, i123] == -1 + assert B2[i34, i345] == -1 + assert B2[i45, i345] == -1 + assert B2[i35, i345] == 1 + assert B2[i24, i245] == 1 + assert B2[i45, i245] == 1 + assert B2[i25, i245] == -1 + assert B2[i23, i235] == 1 + assert B2[i35, i235] == 1 + assert B2[i25, i235] == -1 + assert B2[i23, i234] == 1 + assert B2[i34, i234] == 1 + assert B2[i24, i234] == -1 + + assert B3[i345, i2345] == -1 + assert B3[i245, i2345] == -1 + assert B3[i235, i2345] == 1 + assert B3[i234, i2345] == -1 + assert np.linalg.norm(B1 @ B2) == 0 assert np.linalg.norm(B2 @ B3) == 0 diff --git a/xgi/classes/simplicialcomplex.py b/xgi/classes/simplicialcomplex.py index 8e921df89..055452b71 100644 --- a/xgi/classes/simplicialcomplex.py +++ b/xgi/classes/simplicialcomplex.py @@ -11,7 +11,7 @@ from warnings import warn from ..exception import XGIError -from ..utils.utilities import update_uid_counter +from ..utils.utilities import powerset, update_uid_counter from .hypergraph import Hypergraph from .reportviews import EdgeView, NodeView @@ -150,6 +150,42 @@ def add_node_to_edge(self, edge, node): """add_node_to_edge is not implemented in SimplicialComplex.""" raise XGIError("add_node_to_edge is not implemented in SimplicialComplex.") + def remove_node(self, n, strong=False): + """remove_node is not implemented in SimplicialComplex.""" + raise XGIError("remove_node is not implemented in SimplicialComplex.") + + def _add_simplex(self, members, id=None, **attr): + """Helper function to add a simplex to a simplicial complex, without any + check. Does not automatically update self._edge_uid""" + + self._edge[id] = set() + for node in members: + if node not in self._node: + if node is None: + raise ValueError("None cannot be a node") + self._node[node] = set() + self._node_attr[node] = self._node_attr_dict_factory() + self._node[node].add(id) + + self._edge[id] = members + self._edge_attr[id] = self._hyperedge_attr_dict_factory() + self._edge_attr[id].update(attr) + + def _add_face(self, members): + """Helper function to add a face to a simplicial complex, without any + check, and without attributes. Automatically updates self._edge_uid""" + + id = next(self._edge_uid) + self._edge[id] = frozenset(members) + + for n in members: + if n not in self._node: + self._node[n] = set() + self._node_attr[n] = self._node_attr_dict_factory() + self._node[n].add(id) + + self._edge_attr[id] = self._hyperedge_attr_dict_factory() + def add_simplex(self, members, id=None, **attr): """Add a simplex to the simplicial complex, and all its subfaces that do not exist yet. @@ -188,9 +224,9 @@ def add_simplex(self, members, id=None, **attr): >>> import xgi >>> S = xgi.SimplicialComplex() >>> S.add_simplex([1, 2, 3]) - >>> S.edges.members() # doctest: +NORMALIZE_WHITESPACE - [frozenset({1, 2, 3}), frozenset({1, 2}), - frozenset({1, 3}), frozenset({2, 3})] + >>> S.edges.members() # doctest: +NORMALIZE_WHITESPACE +SKIP + [frozenset({1, 2, 3}), frozenset({2, 3}), + frozenset({1, 2}), frozenset({1, 3})] >>> S.add_simplex([3, 4], id='myedge') >>> S.edges EdgeView((0, 1, 2, 3, 'myedge')) @@ -214,32 +250,30 @@ def add_simplex(self, members, id=None, **attr): if not members: raise XGIError("Cannot add an empty edge") - if not self.has_simplex(members): + if self.has_simplex(members): + return - if id in self._edge.keys(): # check that uid is not present yet - warn(f"uid {id} already exists, cannot add simplex {members}") - return - - uid = next(self._edge_uid) if not id else id - self._edge[uid] = set() - for node in members: - if node not in self._node: - if node is None: - raise ValueError("None cannot be a node") - self._node[node] = set() - self._node_attr[node] = self._node_attr_dict_factory() - self._node[node].add(uid) - - self._edge[uid] = members - self._edge_attr[uid] = self._hyperedge_attr_dict_factory() - self._edge_attr[uid].update(attr) - - if id: # set self._edge_uid correctly - update_uid_counter(self, id) + if id in self._edge.keys(): # check that uid is not present yet + warn(f"uid {id} already exists, cannot add simplex {members}") + return + + id = next(self._edge_uid) if not id else id + + self._add_simplex(members, id, **attr) + + # set self._edge_uid correctly + update_uid_counter(self, id) - # add all subfaces - faces = self._subfaces(members) - self.add_simplices_from(faces) + # add all subfaces + faces = self._subfaces(members) + faces = set(faces) # get unique faces + for members_sub in faces: + + # check that it does not exist yet (based on members, not ID) + if not members_sub or self.has_simplex(members_sub): + continue + + self._add_face(members_sub) def _subfaces(self, simplex, all=True): """Returns list of subfaces of simplex. @@ -328,28 +362,28 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): automatically. >>> S.add_simplices_from([[0, 1], [1, 2], [2, 3, 4]]) - >>> S.edges.members(dtype=dict) + >>> S.edges.members(dtype=dict) # doctest: +SKIP {0: frozenset({0, 1}), 1: frozenset({1, 2}), 2: frozenset({2, 3, 4}), 3: frozenset({2, 3}), 4: frozenset({2, 4}), 5: frozenset({3, 4})} Custom simplex ids can be specified using a dict. >>> S = xgi.SimplicialComplex() >>> S.add_simplices_from({'one': [0, 1], 'two': [1, 2], 'three': [2, 3, 4]}) - >>> S.edges.members(dtype=dict) + >>> S.edges.members(dtype=dict) # doctest: +SKIP {'one': frozenset({0, 1}), 'two': frozenset({1, 2}), 'three': frozenset({2, 3, 4}), 0: frozenset({2, 3}), 1: frozenset({2, 4}), 2: frozenset({3, 4})} You can use the dict format to easily add simplices from another simplicial complex. >>> S2 = xgi.SimplicialComplex() >>> S2.add_simplices_from(S.edges.members(dtype=dict)) - >>> S.edges == S2.edges + >>> list(S.edges) == list(S2.edges) True Alternatively, simplex ids can be specified using an iterable of 2-tuples. >>> S = xgi.SimplicialComplex() >>> S.add_simplices_from([([0, 1], 'one'), ([1, 2], 'two'), ([2, 3, 4], 'three')]) - >>> S.edges.members(dtype=dict) + >>> S.edges.members(dtype=dict) # doctest: +SKIP {'one': frozenset({0, 1}), 'two': frozenset({1, 2}), 'three': frozenset({2, 3, 4}), 0: frozenset({2, 3}), 1: frozenset({2, 4}), 2: frozenset({3, 4})} Attributes for each simplex may be specified using a 2-tuple for each simplex. @@ -381,6 +415,8 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): # format 5 is the easiest one if isinstance(ebunch_to_add, dict): + + faces = [] # container to store subfaces for id, members in ebunch_to_add.items(): # check that it does not exist yet (based on members, not ID) @@ -393,26 +429,31 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): if max_order != None: if len(members) > max_order + 1: - combos = combinations(members, max_order + 1) - self.add_simplices_from(list(combos), max_order=None) + combos = powerset(members, include_singletons=False) + faces += list(combos) continue + try: - self._edge[id] = frozenset(members) + _ = frozenset(members) except TypeError as e: raise XGIError("Invalid ebunch format") from e - for n in members: - if n not in self._node: - self._node[n] = set() - self._node_attr[n] = self._node_attr_dict_factory() - self._node[n].add(id) - self._edge_attr[id] = self._hyperedge_attr_dict_factory() + + self._add_simplex(frozenset(members), id) update_uid_counter(self, id) - # add subfaces - faces = self._subfaces(members) - self.add_simplices_from(faces) + # store subfaces + faces += self._subfaces(members) + + # add subfaces + faces = set(faces) # get unique subfaces + for members in faces: + # check that it does not exist yet (based on members, not ID) + if not members or self.has_simplex(members): + continue + + self._add_face(members) return @@ -446,6 +487,7 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): ): raise XGIError("Members cannot be specified as a string") + faces = [] # now we may iterate over the rest e = first_edge while True: @@ -469,7 +511,6 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): if not members or self.has_simplex(members): try: e = next(new_edges) - except StopIteration: break @@ -479,10 +520,11 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): # we're skipping ID numbers when edges already exist if format1 or format3: id = next(self._edge_uid) + if max_order != None: if len(members) > max_order + 1: - combos = combinations(members, max_order + 1) - self.add_simplices_from(list(combos), max_order=None) + combos = powerset(members, include_singletons=False) + faces += list(combos) # store faces try: e = next(new_edges) @@ -493,36 +535,49 @@ def add_simplices_from(self, ebunch_to_add, max_order=None, **attr): if id in self._edge.keys(): # check that uid is not present yet warn(f"uid {id} already exists, cannot add simplex {members}.") - else: try: - self._edge[id] = frozenset(members) - except TypeError as e: - raise XGIError("Invalid ebunch format") from e + e = next(new_edges) + except StopIteration: + break - for n in members: - if n not in self._node: - self._node[n] = set() - self._node_attr[n] = self._node_attr_dict_factory() - self._node[n].add(id) + continue - self._edge_attr[id] = self._hyperedge_attr_dict_factory() - self._edge_attr[id].update(attr) - self._edge_attr[id].update(eattr) + try: + self._edge[id] = frozenset(members) + except TypeError as e: + raise XGIError("Invalid ebunch format") from e + + for n in members: + if n not in self._node: + self._node[n] = set() + self._node_attr[n] = self._node_attr_dict_factory() + self._node[n].add(id) - if format2 or format4: - update_uid_counter(self, id) + self._edge_attr[id] = self._hyperedge_attr_dict_factory() + self._edge_attr[id].update(attr) + self._edge_attr[id].update(eattr) - # add subfaces - faces = self._subfaces(members) - self.add_simplices_from(faces) + update_uid_counter(self, id) + + # store subfaces + faces += self._subfaces(members) try: e = next(new_edges) except StopIteration: - break + # add subfaces + faces = set(faces) # get unique faces + for members in faces: + + # check that it does not exist yet (based on members, not ID) + if not members or self.has_simplex(members): + continue + + self._add_face(members) + def close(self): """Adds all missing subfaces to the complex. @@ -592,6 +647,21 @@ def add_weighted_simplices_from( except KeyError: XGIError("Empty or invalid simplices specified.") + def _remove_simplex_id(self, id): + """Helper function to remove a simplex with a given id + + Parameters + ---------- + id : Hashable + edge ID to remove + + """ + + for node in self.edges.members(id): + self._node[node].remove(id) + del self._edge[id] + del self._edge_attr[id] + def remove_simplex_id(self, id): """Remove a simplex with a given id. @@ -616,13 +686,11 @@ def remove_simplex_id(self, id): # remove all simplices that contain simplex supfaces_ids = self._supfaces_id(self._edge[id]) - self.remove_simplex_ids_from(supfaces_ids) + for sup_id in supfaces_ids: + self._remove_simplex_id(sup_id) # remove simplex - for node in self.edges.members(id): - self._node[node].remove(id) - del self._edge[id] - del self._edge_attr[id] + self._remove_simplex_id(id) except KeyError as e: raise XGIError(f"Simplex {id} is not in the Simplicialcomplex") from e @@ -647,10 +715,7 @@ def remove_simplex_ids_from(self, ebunch): """ for id in ebunch: - for node in self.edges.members(id): - self._node[node].remove(id) - del self._edge[id] - del self._edge_attr[id] + self.remove_simplex_id(id) def has_simplex(self, simplex): """Whether a simplex appears in the simplicial complex. diff --git a/xgi/utils/utilities.py b/xgi/utils/utilities.py index 7a00f73e6..95a26aaa2 100644 --- a/xgi/utils/utilities.py +++ b/xgi/utils/utilities.py @@ -105,7 +105,7 @@ def update_uid_counter(H, new_id): not isinstance(new_id, str) and not isinstance(new_id, tuple) and float(new_id).is_integer() - and uid < new_id + 1 + and uid <= new_id ): # tuple comes from merging edges and doesn't have as as_integer() method. start = int(new_id) + 1