Skip to content

Commit

Permalink
Cleanup hypergraph methods (#150)
Browse files Browse the repository at this point in the history
* move convert_labels_to_integers from utils/utilities.py to classes/function.py. The reason is that it was causing aboslute/relative import issues
* implement IDView.duplicates
* remove Hypergraph.has_edge and implement IDView.lookup instead
* remove duplicate_edges in favor of H.edges.duplicates
* change the examples section of the IDView.lookup docstring because it was causing problems during CI (in particular, the output was NodeView(('b', 'a')) instead of NodeView(('a', 'b')). Instead, that particular test was moved to the actual test suite.
  • Loading branch information
leotrs authored Aug 22, 2022
1 parent 45f03b5 commit 75d6f9c
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 163 deletions.
10 changes: 0 additions & 10 deletions docs/source/api/classes/xgi.classes.hypergraph.Hypergraph.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
~Hypergraph.remove_node
~Hypergraph.remove_edge
~Hypergraph.remove_nodes_from
~Hypergraph.remove_nodes_from
~Hypergraph.remove_edges_from
~Hypergraph.remove_node_from_edge
~Hypergraph.clear
Expand All @@ -47,12 +46,3 @@

~Hypergraph.copy
~Hypergraph.dual


.. rubric:: Methods that query nodes and edges

.. autosummary::
:nosignatures:

~Hypergraph.duplicate_edges
~Hypergraph.has_edge
2 changes: 2 additions & 0 deletions docs/source/api/classes/xgi.classes.reportviews.EdgeView.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ xgi.classes.reportviews.EdgeView
~EdgeView.members
~EdgeView.singletons
~IDView.neighbors
~IDView.duplicates
~IDView.lookup
~IDView.filterby
~IDView.filterby_attr
2 changes: 2 additions & 0 deletions docs/source/api/classes/xgi.classes.reportviews.IDView.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ xgi.classes.reportviews.IDView

~IDView.from_view
~IDView.neighbors
~IDView.duplicates
~IDView.lookup
~IDView.filterby
~IDView.filterby_attr
2 changes: 2 additions & 0 deletions docs/source/api/classes/xgi.classes.reportviews.NodeView.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ xgi.classes.reportviews.NodeView
~NodeView.memberships
~NodeView.isolates
~IDView.neighbors
~IDView.duplicates
~IDView.lookup
~IDView.filterby
~IDView.filterby_attr
1 change: 0 additions & 1 deletion docs/source/api/classes/xgi.utils.utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@

.. rubric:: Functions

.. autofunction:: convert_labels_to_integers
.. autofunction:: dual_dict
47 changes: 47 additions & 0 deletions tests/classes/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,50 @@ def test_is_empty():
assert xgi.is_empty(H1)
assert xgi.is_empty(H2)
assert not xgi.is_empty(H3)


def test_convert_labels_to_integers(hypergraph1, hypergraph2):
H1 = xgi.convert_labels_to_integers(hypergraph1)
H2 = xgi.convert_labels_to_integers(hypergraph2)
H3 = xgi.convert_labels_to_integers(hypergraph1, "old_ids")

assert set(H1.nodes) == {0, 1, 2}
assert set(H1.edges) == {0, 1, 2}

assert H1.nodes[0]["label"] == "a"
assert H1.nodes[1]["label"] == "b"
assert H1.nodes[2]["label"] == "c"

assert H1.edges[0]["label"] == "e1"
assert H1.edges[1]["label"] == "e2"
assert H1.edges[2]["label"] == "e3"

assert H1.edges.members(0) == [0, 1]
assert H1.edges.members(1) == [0, 1, 2]
assert H1.edges.members(2) == [2]

assert H1.nodes.memberships(0) == [0, 1]
assert H1.nodes.memberships(1) == [0, 1]
assert H1.nodes.memberships(2) == [1, 2]

assert set(H2.nodes) == {0, 1, 2}
assert set(H2.edges) == {0, 1, 2}

assert H2.nodes[0]["label"] == "b"
assert H2.nodes[1]["label"] == "c"
assert H2.nodes[2]["label"] == 0

assert H2.edges[0]["label"] == "e1"
assert H2.edges[1]["label"] == "e2"
assert H2.edges[2]["label"] == "e3"

assert H2.edges.members(0) == [2, 0]
assert H2.edges.members(1) == [2, 1]
assert H2.edges.members(2) == [2, 0, 1]

assert H2.nodes.memberships(0) == [0, 2]
assert H2.nodes.memberships(1) == [1, 2]
assert H2.nodes.memberships(2) == [0, 1, 2]

assert H3.nodes[0]["old_ids"] == "a"
assert H3.edges[0]["old_ids"] == "e1"
43 changes: 33 additions & 10 deletions tests/classes/test_hypergraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,6 @@ def test_members(edgelist1):
H.nodes.memberships(slice(1, 4))


def test_has_edge(edgelist1):
H = xgi.Hypergraph(edgelist1)
assert H.has_edge([1, 2, 3])
assert H.has_edge({1, 2, 3})
assert H.has_edge({4})
assert not H.has_edge([4, 5])
assert not H.has_edge([3])
assert not H.has_edge([1, 2])


def test_add_edge():
for edge in [[1, 2, 3], {1, 2, 3}, iter([1, 2, 3])]:
H = xgi.Hypergraph()
Expand Down Expand Up @@ -395,3 +385,36 @@ def test_double_edge_swap(edgelist1):
# loopy swap
H.double_edge_swap(4, 6, 0, 2)
assert H.edges.members() == [[6, 2, 6], [3], [5, 4], [1, 7, 8]]


def test_duplicate_edges(edgelist1):
H = xgi.Hypergraph(edgelist1)
assert list(H.edges.duplicates()) == []

H.add_edge([1, 3, 2]) # same order as existing edge
assert list(H.edges.duplicates()) == [0, 4]

H.add_edge([1, 2, 3]) # different order, same members
assert list(H.edges.duplicates()) == [0, 4, 5]

H = xgi.Hypergraph([[1, 2, 3, 3], [1, 2, 3]]) # repeated nodes
assert list(H.edges.duplicates()) == []

H = xgi.Hypergraph([[1, 2, 3, 3], [3, 1, 2, 3]]) # repeated nodes
assert list(H.edges.duplicates()) == [0, 1]


def test_duplicate_nodes(edgelist1):
H = xgi.Hypergraph(edgelist1)
assert list(H.nodes.duplicates()) == [1, 2, 3, 7, 8]

H.add_edges_from([[1, 4], [2, 6, 7], [6, 8]])
assert list(H.nodes.duplicates()) == []

# this loop makes 1 and 2 belong to the same edges
for edgeid, members in H.edges.members(dtype=dict).items():
if 1 in members and 2 not in members:
H.add_node_to_edge(edgeid, 2)
if 1 not in members and 2 in members:
H.add_node_to_edge(edgeid, 1)
assert list(H.nodes.duplicates()) == [1, 2]
21 changes: 21 additions & 0 deletions tests/classes/test_reportviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,24 @@ def test_singletons(edgelist1):
H.remove_edge(1)
assert 1 not in H.edges
assert list(H.edges.singletons()) == []


def test_lookup(edgelist1):
H = xgi.Hypergraph(edgelist1)
assert list(H.edges.lookup([1, 2, 3])) == [0]
assert list(H.edges.lookup({1, 2, 3})) == [0]
assert list(H.edges.lookup({4})) == [1]
assert list(H.edges.lookup([4, 5])) == []
assert list(H.edges.lookup([3])) == []
assert list(H.edges.lookup([4])) == [1]
assert list(H.edges.lookup([1, 2])) == []

H = xgi.Hypergraph([["a", "b", "c"], ["a", "b", "e"], ["c", "d", "e"]])
assert set(H.nodes.lookup([0, 1])) == {"a", "b"}


def test_bool(edgelist1):
H = xgi.Hypergraph([])
assert bool(H.edges) is False
H = xgi.Hypergraph(edgelist1)
assert bool(H.edges) is True
51 changes: 2 additions & 49 deletions tests/utils/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from xgi.utils import convert_labels_to_integers, dual_dict
import xgi


def test_dual_dict(dict5):
dual = dual_dict(dict5)
dual = xgi.dual_dict(dict5)
assert dual[0] == [0]
assert dual[1] == [0]
assert dual[2] == [0]
Expand All @@ -12,50 +12,3 @@ def test_dual_dict(dict5):
assert dual[6] == [2, 3]
assert dual[7] == [3]
assert dual[8] == [3]


def test_convert_labels_to_integers(hypergraph1, hypergraph2):
H1 = convert_labels_to_integers(hypergraph1)
H2 = convert_labels_to_integers(hypergraph2)
H3 = convert_labels_to_integers(hypergraph1, "old_ids")

assert set(H1.nodes) == {0, 1, 2}
assert set(H1.edges) == {0, 1, 2}

assert H1.nodes[0]["label"] == "a"
assert H1.nodes[1]["label"] == "b"
assert H1.nodes[2]["label"] == "c"

assert H1.edges[0]["label"] == "e1"
assert H1.edges[1]["label"] == "e2"
assert H1.edges[2]["label"] == "e3"

assert H1.edges.members(0) == [0, 1]
assert H1.edges.members(1) == [0, 1, 2]
assert H1.edges.members(2) == [2]

assert H1.nodes.memberships(0) == [0, 1]
assert H1.nodes.memberships(1) == [0, 1]
assert H1.nodes.memberships(2) == [1, 2]

assert set(H2.nodes) == {0, 1, 2}
assert set(H2.edges) == {0, 1, 2}

assert H2.nodes[0]["label"] == "b"
assert H2.nodes[1]["label"] == "c"
assert H2.nodes[2]["label"] == 0

assert H2.edges[0]["label"] == "e1"
assert H2.edges[1]["label"] == "e2"
assert H2.edges[2]["label"] == "e3"

assert H2.edges.members(0) == [2, 0]
assert H2.edges.members(1) == [2, 1]
assert H2.edges.members(2) == [2, 0, 1]

assert H2.nodes.memberships(0) == [0, 2]
assert H2.nodes.memberships(1) == [1, 2]
assert H2.nodes.memberships(2) == [0, 1, 2]

assert H3.nodes[0]["old_ids"] == "a"
assert H3.edges[0]["old_ids"] == "e1"
9 changes: 5 additions & 4 deletions xgi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import pkg_resources

from . import (
algorithms,
utils,
classes,
algorithms,
convert,
drawing,
generators,
linalg,
readwrite,
stats,
utils,
)
from .algorithms import *
from .utils import *
from .classes import *
from .algorithms import *
from .convert import *
from .drawing import *
from .generators import *
from .linalg import *
from .readwrite import *
from .stats import *
from .utils import *


__version__ = pkg_resources.require("xgi")[0].version
3 changes: 1 addition & 2 deletions xgi/algorithms/centrality.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
from numpy.linalg import norm
from scipy.sparse.linalg import eigsh

from ..classes import is_uniform
from ..classes import is_uniform, convert_labels_to_integers
from ..exception import XGIError
from ..linalg import clique_motif_matrix, incidence_matrix
from ..utils.utilities import convert_labels_to_integers

__all__ = ["CEC_centrality", "HEC_centrality", "ZEC_centrality", "node_edge_centrality"]

Expand Down
2 changes: 1 addition & 1 deletion xgi/classes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .function import *
from .hypergraph import Hypergraph
from .function import *
from .hypergraphviews import subhypergraph
from .simplicialcomplex import SimplicialComplex
43 changes: 43 additions & 0 deletions xgi/classes/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections import Counter

from ..exception import XGIError
from .hypergraph import Hypergraph

__all__ = [
"max_edge_order",
Expand All @@ -21,6 +22,7 @@
"get_edge_attributes",
"is_empty",
"maximal_simplices",
"convert_labels_to_integers",
]


Expand Down Expand Up @@ -615,3 +617,44 @@ def maximal_simplices(SC):
if maximal:
max_simplices.append(i)
return max_simplices


def convert_labels_to_integers(H, label_attribute="label"):
"""Relabel node and edge IDs to be sequential integers.
Parameters
----------
H : Hypergraph
The hypergraph of interest
label_attribute : string, default: "label"
The attribute name that stores the old node and edge labels
Returns
-------
Hypergraph
A new hypergraph with nodes and edges with sequential IDs starting at 0.
The old IDs are stored in the "label" attribute for both nodes and edges.
Notes
-----
The "relabeling" will occur even if the node/edge IDs are sequential.
Because the old IDs are stored in the "label" attribute for both nodes and edges,
the old "label" values (if they exist) will be overwritten.
"""
node_dict = dict(zip(H.nodes, range(H.num_nodes)))
edge_dict = dict(zip(H.edges, range(H.num_edges)))
temp_H = Hypergraph()
temp_H._hypergraph = H._hypergraph.copy()

for node, id in node_dict.items():
temp_H._node[id] = [edge_dict[e] for e in H._node[node]]
temp_H._node_attr[id] = H._node_attr[node].copy()
temp_H._node_attr[id][label_attribute] = node

for edge, id in edge_dict.items():
temp_H._edge[id] = [node_dict[n] for n in H._edge[edge]]
temp_H._edge_attr[id] = H._edge_attr[edge].copy()
temp_H._edge_attr[id][label_attribute] = edge

return temp_H
Loading

0 comments on commit 75d6f9c

Please sign in to comment.