diff --git a/docs/source/conf.py b/docs/source/conf.py index cc69ad53b..b04dc6a55 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -191,19 +191,19 @@ ( master_doc, "xgi.tex", - u"XGI Documentation", - u"Nicholas W. Landry and Leo Torres", + "XGI Documentation", + "Nicholas W. Landry and Leo Torres", "manual", ), ] -man_pages = [(master_doc, "xgi", u"XGI Documentation", [author], 1)] +man_pages = [(master_doc, "xgi", "XGI Documentation", [author], 1)] texinfo_documents = [ ( master_doc, "XGI", - u"XGI Documentation", + "XGI Documentation", author, "XGI", "One line description of project.", diff --git a/requirements/developer.txt b/requirements/developer.txt index de36c69c7..3947e78bb 100644 --- a/requirements/developer.txt +++ b/requirements/developer.txt @@ -1,4 +1,4 @@ -black==21.5b1 +black==22.3.0 pre-commit>=2.12 isort==5.10.1 pylint>=2.10 \ No newline at end of file diff --git a/tests/algorithms/test_assortativity.py b/tests/algorithms/test_assortativity.py new file mode 100644 index 000000000..fd567efcc --- /dev/null +++ b/tests/algorithms/test_assortativity.py @@ -0,0 +1,60 @@ +import numpy as np +import pytest + +import xgi +from xgi.algorithms.assortativity import choose_degrees +from xgi.exception import XGIError + + +def test_dynamical_assortativity(edgelist1, edgelist6): + + H = xgi.Hypergraph() + with pytest.raises(XGIError): + xgi.dynamical_assortativity(H) + + H.add_nodes_from([0, 1, 2]) + + with pytest.raises(XGIError): + xgi.dynamical_assortativity(H) + + with pytest.raises(XGIError): + H1 = xgi.Hypergraph(edgelist1) + xgi.dynamical_assortativity(H1) + + H1 = xgi.Hypergraph(edgelist6) + + assert abs(xgi.dynamical_assortativity(H1) - -0.0526) < 1e-3 + + +def test_degree_assortativity(edgelist1, edgelist6): + H1 = xgi.Hypergraph(edgelist1) + assert -1 <= xgi.degree_assortativity(H1, kind="uniform") <= 1 + assert -1 <= xgi.degree_assortativity(H1, kind="top-2") <= 1 + assert -1 <= xgi.degree_assortativity(H1, kind="top-bottom") <= 1 + + H2 = xgi.Hypergraph(edgelist6) + assert -1 <= xgi.degree_assortativity(H2, kind="uniform") <= 1 + assert -1 <= xgi.degree_assortativity(H2, kind="top-2") <= 1 + assert -1 <= xgi.degree_assortativity(H2, kind="top-bottom") <= 1 + + +def test_choose_degrees(edgelist1, edgelist6): + H1 = xgi.Hypergraph(edgelist1) + k = H1.degree() + + with pytest.raises(XGIError): + e = H1.edges.members(1) + choose_degrees(e, k) + + e = H1.edges.members(0) + assert np.all(np.array(choose_degrees(e, k)) == 1) + + e = H1.edges.members(3) + assert set(choose_degrees(e, k, kind="top-2")) == {1, 2} + assert set(choose_degrees(e, k, kind="top-bottom")) == {1, 2} + + H2 = xgi.Hypergraph(edgelist6) + e = H2.edges.members(2) + k = H2.degree() + assert set(choose_degrees(e, k, kind="top-2")) == {2, 3} + assert set(choose_degrees(e, k, kind="top-bottom")) == {1, 3} diff --git a/tests/conftest.py b/tests/conftest.py index f94c296c2..3cac9d6d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ -import xgi import networkx as nx import numpy as np import pandas as pd import pytest +import xgi + @pytest.fixture def edgelist1(): diff --git a/tests/stats/test_edgestats.py b/tests/stats/test_edgestats.py index 0c42ee2b5..81f3e911c 100644 --- a/tests/stats/test_edgestats.py +++ b/tests/stats/test_edgestats.py @@ -1,5 +1,6 @@ -import pytest import pandas as pd +import pytest + import xgi from xgi.exception import IDNotFound diff --git a/tests/stats/test_nodestats.py b/tests/stats/test_nodestats.py index 421a90e66..d74d09035 100644 --- a/tests/stats/test_nodestats.py +++ b/tests/stats/test_nodestats.py @@ -1,5 +1,6 @@ -import pytest import pandas as pd +import pytest + import xgi from xgi.exception import IDNotFound diff --git a/xgi/__init__.py b/xgi/__init__.py index 3c4d920ec..1a1f1f5eb 100644 --- a/xgi/__init__.py +++ b/xgi/__init__.py @@ -8,8 +8,8 @@ generators, linalg, readwrite, - utils, stats, + utils, ) from .algorithms import * from .classes import * @@ -18,7 +18,7 @@ from .generators import * from .linalg import * from .readwrite import * -from .utils import * from .stats import * +from .utils import * __version__ = pkg_resources.require("xgi")[0].version diff --git a/xgi/algorithms/__init__.py b/xgi/algorithms/__init__.py index b07bf7399..3798a1212 100644 --- a/xgi/algorithms/__init__.py +++ b/xgi/algorithms/__init__.py @@ -1,2 +1,3 @@ -from . import connected +from . import assortativity, connected +from .assortativity import * from .connected import * diff --git a/xgi/algorithms/assortativity.py b/xgi/algorithms/assortativity.py new file mode 100644 index 000000000..87fe87b7a --- /dev/null +++ b/xgi/algorithms/assortativity.py @@ -0,0 +1,155 @@ +import random +from itertools import combinations + +import numpy as np + +import xgi +from xgi.exception import XGIError + +__all__ = ["dynamical_assortativity", "degree_assortativity"] + + +def dynamical_assortativity(H): + """Computes the dynamical assortativity of a uniform hypergraph. + + Parameters + ---------- + H : xgi.Hypergraph + Hypergraph of interest + + Returns + ------- + float + The dynamical assortativity + + Raises + ------ + XGIError + If the hypergraph is not uniform, or if there are no nodes + or no edges + + References + ---------- + Nicholas Landry and Juan G. Restrepo, + Hypergraph assortativity: A dynamical systems perspective, + Chaos 2022. + DOI: 10.1063/5.0086905 + + """ + if not xgi.is_uniform(H): + raise XGIError("Hypergraph must be uniform!") + + if H.num_nodes == 0 or H.num_edges == 0: + raise XGIError("Hypergraph must contain nodes and edges!") + + degs = H.degree() + k1 = np.mean(list(degs.values())) + k2 = np.mean(np.power(list(degs.values()), 2)) + kk1 = np.mean( + [ + degs[n1] * degs[n2] + for e in H.edges + for n1, n2 in combinations(H.edges.members(e), 2) + ] + ) + + return kk1 * k1**2 / k2**2 - 1 + + +def degree_assortativity(H, kind="uniform", exact=False, num_samples=1000): + """Computes the degree assortativity of a hypergraph + + Parameters + ---------- + H : Hypergraph + The hypergraph of interest + kind : str, default: "uniform" + the type of degree assortativity. valid choices are + "uniform", "top-2", and "top-bottom". + exact : bool, default: False + whether to compute over all edges or + sample randomly from the set of edges + num_samples : int, default: 1000 + if not exact, specify the number of samples for the computation. + + Returns + ------- + float + the degree assortativity + + References + ---------- + Phil Chodrow, + Configuration models of random hypergraphs, + Journal of Complex Networks 2020. + DOI: 10.1093/comnet/cnaa018 + """ + degs = H.degree() + if exact: + k1k2 = [ + choose_degrees(H.edges.members(e), degs, kind) + for e in H.edges + if len(H.edges.members(e)) > 1 + ] + else: + edges = [e for e in H.edges if len(H.edges.members(e)) > 1] + k1k2 = [ + choose_degrees(H.edges.members(random.choice(edges)), degs, kind) + for _ in range(num_samples) + ] + return np.corrcoef(np.array(k1k2).T)[0, 1] + + +def choose_degrees(e, k, kind="uniform"): + """Choose the degrees of two nodes in a hyperedge. + + Parameters + ---------- + e : iterable + the members in a hyperedge + k : dict + the degrees where keys are node IDs and values are degrees + kind : str, default: "uniform" + the type of degree assortativity, options are + "uniform", "top-2", and "top-bottom". + + Returns + ------- + tuple + two degrees selected from the edge + + Raises + ------ + XGIError + if invalid assortativity function chosen + + References + ---------- + Phil Chodrow, + Configuration models of random hypergraphs, + Journal of Complex Networks 2020. + DOI: 10.1093/comnet/cnaa018 + """ + if len(e) > 1: + if kind == "uniform": + i = np.random.randint(len(e)) + j = i + while i == j: + j = np.random.randint(len(e)) + return (k[e[i]], k[e[j]]) + + elif kind == "top-2": + degs = sorted([k[i] for i in e])[-2:] + random.shuffle(degs) + return degs + + elif kind == "top-bottom": + # this selects the largest and smallest degrees in one line + degs = sorted([k[i] for i in e])[:: len(e) - 1] + random.shuffle(degs) + return degs + + else: + raise XGIError("Invalid choice function!") + else: + raise XGIError("Edge must have more than one member!") diff --git a/xgi/classes/reportviews.py b/xgi/classes/reportviews.py index 2a730aeb8..02c5bdf3d 100644 --- a/xgi/classes/reportviews.py +++ b/xgi/classes/reportviews.py @@ -7,10 +7,8 @@ """ from collections.abc import Mapping, Set -import numpy as np - -from xgi.stats import NodeStatDispatcher, EdgeStatDispatcher from xgi.exception import IDNotFound, XGIError +from xgi.stats import EdgeStatDispatcher, NodeStatDispatcher __all__ = [ "NodeView", diff --git a/xgi/generators/nonuniform.py b/xgi/generators/nonuniform.py index 2e606675c..00710bc18 100644 --- a/xgi/generators/nonuniform.py +++ b/xgi/generators/nonuniform.py @@ -8,8 +8,8 @@ import numpy as np from ..classes import SimplicialComplex -from ..utils import py_random_state, np_random_state -from .classic import empty_hypergraph, empty_simplicial_complex +from ..utils import np_random_state, py_random_state +from .classic import empty_hypergraph, lattice __all__ = [ "chung_lu_hypergraph", diff --git a/xgi/stats/__init__.py b/xgi/stats/__init__.py index 0f01ee8f4..6c212c340 100644 --- a/xgi/stats/__init__.py +++ b/xgi/stats/__init__.py @@ -44,16 +44,15 @@ """ +from collections import defaultdict +from typing import Callable + import numpy as np import pandas as pd -from typing import Callable -from collections import defaultdict from xgi.exception import IDNotFound -from . import nodestats -from . import edgestats - +from . import edgestats, nodestats __all__ = ["nodestat_func", "edgestat_func", "EdgeStatDispatcher", "NodeStatDispatcher"]