-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add encapsulation dag functionality (#444)
* Added weights option to to_line_graph function. * Added tests for weighted line graph implementation. * Added s=2 test. * Added encapsulation dag code. * Added encapsulation dag tests. * Removed unnecessary combinations import. * Updated docstring. * included functions from publication (#400) * feat: added shuffle_hyperedges + tests * feat: added shuffle_hyperedges + tests * fix: filenames * fix: tests * style: black and isort * feat: added function node_swap + test * style: black and isort * fix: error from python 3.11 * attempt at listing the available statistics (#405) * attempt at listing the available statistics * fixing previous errors * Attempt at implementing suggestions by nich * second attempt at displaying the titles * another trial * titles in bold * update of the panel for dihypergraph stats * minor fixes * change the hierarchical structure * minor change * moving MultiDi-stats in the right place * adding decorators to the admonition box * moving up the decorators * up-version * feat: enforce equal aspect for circular layout #430 (#432) * feat: enforce equal aspect for circular layout #430 * changed implementation of set_aspect * Refactor draw module (#435) * refact: draw module * style: black and isort * fix #331 (#438) * Added the ability for users to access the optional arguments of NetworkX layout functions. (#439) * Added another test. * Refactored relations to subset_types. Exposed and documented empirical relations filtering function. Documentation improvements. * Update xgi/convert/encapsulation_dag.py documentation Co-authored-by: Maxime Lucas <[email protected]> * Fix typo in xgi/convert/encapsulation_dag.py Co-authored-by: Maxime Lucas <[email protected]> * Updates to documentation. * Minor refactor. --------- Co-authored-by: Maxime Lucas <[email protected]> Co-authored-by: Thomas Robiglio <[email protected]> Co-authored-by: Nicholas Landry <[email protected]>
- Loading branch information
1 parent
52e6aac
commit 7f93d0e
Showing
3 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from networkx import DiGraph | ||
|
||
import xgi | ||
|
||
|
||
def test_to_encapsulation_dag(edgelist1, edgelist8, hypergraph1, hypergraph2): | ||
|
||
H = xgi.Hypergraph(edgelist1) | ||
for subset_types in ["all", "immediate", "empirical"]: | ||
L = xgi.to_encapsulation_dag(H, subset_types=subset_types) | ||
assert isinstance(L, DiGraph) | ||
assert set(L.nodes) == set(list(range(4))) | ||
assert len(L.edges) == 0 | ||
|
||
H = xgi.Hypergraph(edgelist8) | ||
L = xgi.to_encapsulation_dag(H, subset_types="immediate") | ||
assert isinstance(L, DiGraph) | ||
assert set(L.nodes) == set(list(range(9))) | ||
assert len(L.edges) == 1 | ||
assert len(L[3]) == 0 | ||
assert len(list(L.predecessors(0))) == 1 | ||
assert len(list(L.successors(1))) == 1 | ||
|
||
|
||
L = xgi.to_encapsulation_dag(H, subset_types="empirical") | ||
assert isinstance(L, DiGraph) | ||
assert set(L.nodes) == set(list(range(9))) | ||
assert len(L.edges) == 4 | ||
assert len(L[3]) == 3 | ||
assert len(list(L.predecessors(0))) == 1 | ||
assert len(list(L.successors(1))) == 1 | ||
|
||
H = xgi.Hypergraph(edgelist8) | ||
L = xgi.to_encapsulation_dag(H) | ||
|
||
assert isinstance(L, DiGraph) | ||
assert set(L.nodes) == set(list(range(9))) | ||
assert len(L.edges) == 5 | ||
assert len(L[3]) == 4 | ||
assert len(list(L.predecessors(0))) == 2 | ||
|
||
L = xgi.to_encapsulation_dag(H, subset_types="immediate") | ||
assert isinstance(L, DiGraph) | ||
assert set(L.nodes) == set(list(range(9))) | ||
assert len(L.edges) == 1 | ||
assert len(L[3]) == 0 | ||
assert len(list(L.predecessors(0))) == 1 | ||
assert len(list(L.successors(1))) == 1 | ||
|
||
|
||
L = xgi.to_encapsulation_dag(H, subset_types="empirical") | ||
assert isinstance(L, DiGraph) | ||
assert set(L.nodes) == set(list(range(9))) | ||
assert len(L.edges) == 4 | ||
assert len(L[3]) == 3 | ||
assert len(list(L.predecessors(0))) == 1 | ||
assert len(list(L.successors(1))) == 1 | ||
|
||
L = xgi.to_encapsulation_dag(hypergraph1) | ||
assert set(L.nodes) == {"e1", "e2", "e3"} | ||
assert [e for e in L.edges] == [("e2", "e1"), ("e2", "e3")] | ||
|
||
L = xgi.to_encapsulation_dag(hypergraph2) | ||
assert set(L.nodes) == {"e1", "e2", "e3"} | ||
assert [e for e in L.edges] == [("e3", "e1"), ("e3", "e2")] | ||
|
||
H = xgi.Hypergraph() | ||
L = xgi.to_encapsulation_dag(H) | ||
|
||
assert L.number_of_nodes() == 0 | ||
assert L.number_of_edges() == 0 | ||
|
||
H.add_nodes_from([0, 1, 2]) | ||
L = xgi.to_encapsulation_dag(H) | ||
|
||
assert L.number_of_nodes() == 0 | ||
assert L.number_of_edges() == 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
from ..exception import XGIError | ||
|
||
import networkx as nx | ||
|
||
__all__ = ["to_encapsulation_dag", "empirical_subsets_filter"] | ||
|
||
def to_encapsulation_dag(H, subset_types="all"): | ||
"""The encapsulation DAG (Directed Acyclic Graph) of | ||
the hypergraph H. | ||
An encapsulation DAG is a directed line graph | ||
where the nodes are hyperedges in H and a directed edge | ||
exists from a larger hyperedge to a smaller hyperedge if | ||
the smaller is a subset of the larger. | ||
Parameters | ||
---------- | ||
H : Hypergraph | ||
The hypergraph of interest | ||
subset_types : str, optional | ||
Type of relations to include. Options are: | ||
"all" : all subset relationships | ||
"immediate" : only subset relationships between hyperedges of | ||
adjacent sizes (i.e., edges from k to k-1) | ||
"empirical" : A relaxation of the "immediate" option where only | ||
subset relationships between hyperedges of size k and subsets | ||
of maximum size k'<k existing in the hypergraph are included. | ||
For example, a hyperedge of size 5 may have no immediate | ||
encapsulation relationships with hyperedges of size 4, but may | ||
encapsulate hyperedegs of size 3, which will be included if | ||
using this setting (whereas relationships with subsets of size 2 | ||
would not be included). | ||
Returns | ||
------- | ||
LG : networkx.DiGraph | ||
The line graph associated to the Hypergraph | ||
Examples | ||
-------- | ||
>>> import xgi | ||
>>> from xgi.convert import to_encapsulation_dag, empirical_subsets_filter | ||
>>> H = xgi.Hypergraph([["a","b","c"], ["b","c","f"], ["a","b"], ["c", "e"], ["a"], ["f"]]) | ||
>>> dag = to_encapsulation_dag(H) | ||
>>> dag.edges() | ||
OutEdgeView([(0, 2), (0, 4), (2, 4), (1, 5)]) | ||
>>> dag = to_encapsulation_dag(H, subset_types="immediate") | ||
>>> dag.edges() | ||
OutEdgeView([(0, 2), (2, 4)]) | ||
>>> dag = to_encapsulation_dag(H, subset_types="empirical") | ||
>>> dag.edges() | ||
OutEdgeView([(0, 2), (2, 4), (1, 5)]) | ||
References | ||
---------- | ||
"Encapsulation Structure and Dynamics in Hypergraphs", by Timothy LaRock | ||
& Renaud Lambiotte. https://arxiv.org/abs/2307.04613 | ||
""" | ||
if not (subset_types in ["all", "immediate", "empirical"]): | ||
raise XGIError(f"{subset_types} not a valid subset_types option. Choices are " | ||
"'all', 'immediate', and 'empirical'.") | ||
|
||
# Construct the dag | ||
dag = nx.DiGraph() | ||
# Loop over hyperedges | ||
for he_idx in H.edges: | ||
# Add the hyperedge as a node | ||
dag.add_node(he_idx) | ||
# Get the hyperedge as a set | ||
he = H.edges.members(he_idx) | ||
# Get candidate encapsulation hyperedges | ||
candidates = _get_candidates(subset_types, H, he) | ||
# for each candidate | ||
candidates_checked = set() | ||
for cand_idx in candidates: | ||
if cand_idx in candidates_checked: | ||
continue | ||
cand = H.edges.members(cand_idx) | ||
if len(he) > len(cand): | ||
if _encapsulated(he, cand): | ||
dag.add_edge(he_idx, cand_idx) | ||
elif len(cand) > len(he): | ||
if _encapsulated(cand, he): | ||
dag.add_edge(cand_idx, he_idx) | ||
|
||
# If empirically closest subsets, filter out all edges except those | ||
# between k and maximum existing k'<k | ||
if subset_types == "empirical": | ||
empirical_subsets_filter(H, dag) | ||
|
||
return dag | ||
|
||
def _encapsulated(larger, smaller): | ||
"""Test if a larger hyperedge encapsulates a smaller hyperedge. | ||
Parameters | ||
---------- | ||
larger : Set | ||
larger hyperedge | ||
smaller : Set | ||
smaller hyperedge | ||
Returns | ||
------- | ||
_ : Bool | ||
True if the size of the intersection between larger and smaller is | ||
the same as the size of smaller. | ||
""" | ||
return len(set(larger).intersection(set(smaller))) == len(smaller) | ||
|
||
def _get_candidates(subset_types, H, he): | ||
"""Get the candidate hyperedges for encapsulation by he based on the subset type. | ||
Parameters | ||
---------- | ||
subset_types : str | ||
Type of subset relationships | ||
H : Hypergraph | ||
The hypergraph | ||
he : Set | ||
The hyperedge | ||
Returns | ||
------- | ||
candidates : Set | ||
A set of hyperedge IDs to check for encapsulation by hyperedge he | ||
""" | ||
candidates = set() | ||
for node in he: | ||
for cand_idx in H.nodes.memberships(node): | ||
if cand_idx not in candidates: | ||
cand = H.edges.members(cand_idx) | ||
if _check_candidate(subset_types, he, cand): | ||
candidates.add(cand_idx) | ||
|
||
return candidates | ||
|
||
def _check_candidate(subset_types, he, cand): | ||
"""Check whether a hyperedge cand is a candidate to be encapsulated by | ||
hyperedge he based on subset_types. | ||
Parameters | ||
---------- | ||
subset_types : str | ||
Type of subset relationships | ||
he : Set | ||
The hyperedge | ||
cand : Set | ||
The candidate | ||
Returns | ||
------- | ||
is_candidate : bool | ||
True if cand is a valid candidate to be encapsulated by he, False | ||
otherwise. | ||
""" | ||
is_candidate = False | ||
if subset_types in ["all", "empirical"]: | ||
if len(he) != len(cand): | ||
is_candidate = True | ||
elif subset_types == "immediate": | ||
if len(he) == len(cand)-1 or len(he)-1 == len(cand): | ||
is_candidate = True | ||
return is_candidate | ||
|
||
def empirical_subsets_filter(H, dag): | ||
""" | ||
Filters encapsulation DAG of H in place to only include edges between hyperedges | ||
of size k and the maximum existing k'. | ||
Parameters | ||
---------- | ||
H : Hypergraph | ||
The hypergraph of interest | ||
dag : nx.DiGraph | ||
The encapsulation dag of H constructed with to_encapsulation_dag(H, subset_types="all") | ||
Returns | ||
------- | ||
dag : networkx.DiGraph | ||
The filtered line graph (also modified in place) | ||
References | ||
---------- | ||
"Encapsulation Structure and Dynamics in Hypergraphs", by Timothy LaRock | ||
& Renaud Lambiotte. https://arxiv.org/abs/2307.04613 | ||
""" | ||
# Loop over all edges | ||
for edge_idx in dag: | ||
preds = list(dag.predecessors(edge_idx)) | ||
if len(preds) > 0: | ||
# Get the minimum superface size | ||
min_sup_size = min([len(H.edges.members(cand_idx)) for cand_idx in | ||
preds]) | ||
# Keep only the superfaces with that size | ||
to_remove = [] | ||
for cand_idx in preds: | ||
if len(H.edges.members(cand_idx)) != min_sup_size: | ||
dag.remove_edge(cand_idx, edge_idx) | ||
|
||
# Repeat for subsets | ||
outs = list(dag.successors(edge_idx)) | ||
if len(outs) > 0: | ||
max_sub_size = max([len(H.edges.members(sub_idx)) for sub_idx in outs]) | ||
for cand_idx in outs: | ||
if len(H.edges.members(cand_idx)) != max_sub_size: | ||
dag.remove_edge(edge_idx, cand_idx) | ||
return dag |