chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import easygraph.classes
|
||||
import easygraph.convert
|
||||
import easygraph.datapipe
|
||||
import easygraph.datasets
|
||||
import easygraph.exception
|
||||
import easygraph.experiments
|
||||
import easygraph.functions
|
||||
import easygraph.ml_metrics
|
||||
import easygraph.model
|
||||
import easygraph.nn
|
||||
import easygraph.readwrite
|
||||
import easygraph.utils
|
||||
|
||||
from easygraph.classes import *
|
||||
from easygraph.convert import *
|
||||
from easygraph.datapipe import *
|
||||
from easygraph.datasets import *
|
||||
from easygraph.exception import *
|
||||
from easygraph.experiments import *
|
||||
from easygraph.functions import *
|
||||
from easygraph.ml_metrics import *
|
||||
from easygraph.model import *
|
||||
from easygraph.nn import *
|
||||
from easygraph.readwrite import *
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
print(f"attr {name} doesn't exist!")
|
||||
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_eg_cache_root():
|
||||
root = Path.home() / Path(".easygraph/")
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
return root
|
||||
|
||||
|
||||
AUTHOR_EMAIL = "bdye22@m.fudan.edu.cn"
|
||||
# global paths
|
||||
CACHE_ROOT = get_eg_cache_root()
|
||||
DATASETS_ROOT = CACHE_ROOT / "datasets"
|
||||
REMOTE_ROOT = "https://download.moon-lab.tech:28501/"
|
||||
REMOTE_DATASETS_ROOT = REMOTE_ROOT + "datasets/"
|
||||
@@ -0,0 +1,19 @@
|
||||
from .directed_graph import DiGraph
|
||||
from .directed_graph import DiGraphC
|
||||
from .directed_multigraph import MultiDiGraph
|
||||
from .graph import Graph
|
||||
from .graph import GraphC
|
||||
from .graphviews import *
|
||||
from .multigraph import MultiGraph
|
||||
from .operation import *
|
||||
|
||||
|
||||
try:
|
||||
from .base import BaseHypergraph
|
||||
from .base import load_structure
|
||||
from .hypergraph import Hypergraph
|
||||
except:
|
||||
print(
|
||||
"Warning raise in module:classes. Please install Pytorch before you use"
|
||||
" functions related to Hypergraph"
|
||||
)
|
||||
@@ -0,0 +1,996 @@
|
||||
import abc
|
||||
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["load_structure", "BaseHypergraph"]
|
||||
|
||||
|
||||
def load_structure(file_path: Union[str, Path]):
|
||||
r"""Load a EasyGraph's high-order network structure from a file. The supported structure ``Hypergraph``.
|
||||
|
||||
Args:
|
||||
``file_path`` (``Union[str, Path]``): The file path to load the EasyGraph's structure.
|
||||
"""
|
||||
import pickle as pkl
|
||||
|
||||
import easygraph
|
||||
|
||||
file_path = Path(file_path)
|
||||
assert file_path.exists(), f"{file_path} does not exist"
|
||||
with open(file_path, "rb") as f:
|
||||
data = pkl.load(f)
|
||||
class_name, state_dict = data["class"], data["state_dict"]
|
||||
structure_class = getattr(easygraph, class_name)
|
||||
structure = structure_class.from_state_dict(state_dict)
|
||||
return structure
|
||||
|
||||
|
||||
class BaseHypergraph:
|
||||
r"""The ``BaseHypergraph`` class is the base class for all hypergraph structures.
|
||||
|
||||
Args:
|
||||
``num_v`` (``int``): The number of vertices.
|
||||
``e_list`` (``Union[List[int], List[List[int]]], optional``): Edge list. Defaults to ``None``.
|
||||
``e_weight`` (``Union[float, List[float]], optional``): A list of weights for edges. Defaults to ``None``.
|
||||
``extra_selfloop`` (``bool``, optional): Whether to add extra self-loop to the graph. Defaults to ``False``.
|
||||
``device`` (``torch.device``, optional): The device to store the graph. Defaults to ``torch.device('cpu')``.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_v: int,
|
||||
v_property: Optional[Union[Dict, List[Dict]]] = None,
|
||||
e_list: Optional[Union[List[int], List[List[int]]]] = None,
|
||||
e_property: Optional[Union[Dict, List[Dict]]] = None,
|
||||
e_weight: Optional[Union[float, List[float]]] = None,
|
||||
extra_selfloop: bool = False,
|
||||
device: str = "cpu",
|
||||
):
|
||||
assert (
|
||||
isinstance(num_v, int) and num_v > 0
|
||||
), "num_v should be a positive integer"
|
||||
self.clear()
|
||||
self._num_v = num_v
|
||||
# self.device = torch.cuda.device(device)
|
||||
if v_property == None:
|
||||
self._v_property = [{} for i in range(num_v)]
|
||||
else:
|
||||
v_property = self._format_v_property_list(num_v, v_property)
|
||||
self._v_property = v_property
|
||||
|
||||
if e_property == None and e_list != None:
|
||||
self._e_property = [{} for i in range(len(e_list))]
|
||||
elif e_property != None and e_list != None:
|
||||
e_property = self._format_e_property_list(len(e_list), e_property)
|
||||
self._e_property = e_property
|
||||
|
||||
self._has_extra_selfloop = extra_selfloop
|
||||
|
||||
@abc.abstractmethod
|
||||
def __repr__(self) -> str:
|
||||
r"""Print the hypergraph information."""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def state_dict(self) -> Dict[str, Any]:
|
||||
r"""Get the state dict of the hypergraph."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def save(self, file_path: Union[str, Path]):
|
||||
r"""Save the EasyGraph's hypergraph structure to a file.
|
||||
|
||||
Args:
|
||||
``file_path`` (``str``): The file_path to store the EasyGraph's hypergraph structure.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def load(file_path: Union[str, Path]):
|
||||
r"""Load the EasyGraph's hypergraph structure from a file.
|
||||
|
||||
Args:
|
||||
``file_path`` (``str``): The file path to load the DEasyGraph's hypergraph structure.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def from_state_dict(state_dict: dict):
|
||||
r"""Load the EasyGraph's hypergraph structure from the state dict.
|
||||
|
||||
Args:
|
||||
``state_dict`` (``dict``): The state dict to load the EasyGraph's hypergraph.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def draw(self, **kwargs):
|
||||
r"""Draw the structure."""
|
||||
|
||||
def clear(self):
|
||||
r"""Remove all hyperedges and caches from the hypergraph."""
|
||||
self._clear_raw()
|
||||
self._clear_cache()
|
||||
|
||||
def _clear_raw(self):
|
||||
self._v_weight = None
|
||||
self._raw_groups = {}
|
||||
|
||||
def _clear_cache(self, group_name: Optional[str] = None):
|
||||
r"""Clear the cache."""
|
||||
self.cache = {}
|
||||
if group_name is None:
|
||||
self.group_cache = defaultdict(dict)
|
||||
else:
|
||||
self.group_cache.pop(group_name, None)
|
||||
|
||||
@abc.abstractmethod
|
||||
def clone(self) -> "BaseHypergraph":
|
||||
r"""Return a copy of this type of hypergraph."""
|
||||
|
||||
def to(self, device: str = "cpu") -> "BaseHypergraph":
|
||||
r"""Move the hypergraph to the specified device.
|
||||
|
||||
Args:
|
||||
``device`` (``torch.device``): The device to store the hypergraph.
|
||||
"""
|
||||
# self.device = torch.device
|
||||
for v in self.vars_for_DL:
|
||||
if v in self.cache and self.cache[v] is not None:
|
||||
self.cache[v] = self.cache[v].to(device)
|
||||
for name in self.group_names:
|
||||
if (
|
||||
v in self.group_cache[name]
|
||||
and self.group_cache[name][v] is not None
|
||||
):
|
||||
self.group_cache[name][v] = self.group_cache[name][v].to(device)
|
||||
return self
|
||||
|
||||
# utils
|
||||
def _hyperedge_code(self, src_v_set: List[int], dst_v_set: List[int]) -> Tuple:
|
||||
r"""Generate the hyperedge code.
|
||||
|
||||
Args:
|
||||
``src_v_set`` (``List[int]``): The source vertex set.
|
||||
``dst_v_set`` (``List[int]``): The destination vertex set.
|
||||
"""
|
||||
return tuple([src_v_set, dst_v_set])
|
||||
|
||||
def _merge_hyperedges(self, e1: dict, e2: dict, op: str = "mean"):
|
||||
assert op in [
|
||||
"mean",
|
||||
"sum",
|
||||
"max",
|
||||
], "Hyperedge merge operation must be one of ['mean', 'sum', 'max']"
|
||||
_func = {
|
||||
"mean": lambda x, y: (x + y) / 2,
|
||||
"sum": lambda x, y: x + y,
|
||||
"max": lambda x, y: max(x, y),
|
||||
}
|
||||
_e = {}
|
||||
if "w_v2e" in e1 and "w_v2e" in e2:
|
||||
for _idx in range(len(e1["w_v2e"])):
|
||||
_e["w_v2e"] = _func[op](e1["w_v2e"][_idx], e2["w_v2e"][_idx])
|
||||
if "w_e2v" in e1 and "w_e2v" in e2:
|
||||
for _idx in range(len(e1["w_e2v"])):
|
||||
_e["w_e2v"] = _func[op](e1["w_e2v"][_idx], e2["w_e2v"][_idx])
|
||||
_e["w_e"] = _func[op](e1["w_e"], e2["w_e"])
|
||||
return _e
|
||||
|
||||
@staticmethod
|
||||
def _format_e_list(e_list: Union[List[int], List[List[int]]]) -> List[List[int]]:
|
||||
r"""Format the hyperedge list.
|
||||
|
||||
Args:
|
||||
``e_list`` (``List[int]`` or ``List[List[int]]``): The hyperedge list.
|
||||
"""
|
||||
if len(e_list) == 0:
|
||||
pass
|
||||
elif type(e_list[0]) in (int, float):
|
||||
return [tuple(sorted(e_list))]
|
||||
elif type(e_list) == tuple:
|
||||
e_list = list(e_list)
|
||||
elif type(e_list) == list:
|
||||
pass
|
||||
else:
|
||||
raise TypeError("e_list must be List[int] or List[List[int]].")
|
||||
for _idx in range(len(e_list)):
|
||||
e_list[_idx] = tuple(sorted(list(set(e_list[_idx]))))
|
||||
|
||||
return e_list
|
||||
|
||||
def _format_e_property_list(self, e_num, e_property_list: Union[Dict, List[Dict]]):
|
||||
r"""Format the property list.
|
||||
|
||||
Args:
|
||||
``e_list`` (``Dict`` or ``List[Dict]``): The property list.
|
||||
"""
|
||||
if type(e_property_list) == dict:
|
||||
return [e_property_list]
|
||||
elif type(e_property_list) == list and len(e_property_list) != e_num:
|
||||
raise EasyGraphError(
|
||||
"The length of property list must be equal to edge number"
|
||||
)
|
||||
elif type(e_property_list) == list:
|
||||
pass
|
||||
else:
|
||||
raise TypeError("e_property_list must be Dict or List[Dict].")
|
||||
|
||||
return e_property_list
|
||||
|
||||
def _format_v_property_list(self, v_num, v_property_list: Union[Dict, List[Dict]]):
|
||||
r"""Format the property list.
|
||||
|
||||
Args:
|
||||
``e_list`` (``Dict`` or ``List[Dict]``): The property list.
|
||||
"""
|
||||
if type(v_property_list) == dict:
|
||||
return [v_property_list]
|
||||
elif type(v_property_list) == list and len(v_property_list) != v_num:
|
||||
raise EasyGraphError(
|
||||
"The length of property list must be equal to node number"
|
||||
)
|
||||
elif type(v_property_list) == list:
|
||||
pass
|
||||
else:
|
||||
raise TypeError("v_property_list must be Dict or List[Dict].")
|
||||
|
||||
return v_property_list
|
||||
|
||||
@staticmethod
|
||||
def _format_e_list_and_w_on_them(
|
||||
e_list: Union[List[int], List[List[int]]],
|
||||
w_list: Optional[Union[List[int], List[List[int]]]] = None,
|
||||
):
|
||||
r"""Format ``e_list`` and ``w_list``.
|
||||
|
||||
Args:
|
||||
``e_list`` (Union[List[int], List[List[int]]]): Hyperedge list.
|
||||
``w_list`` (Optional[Union[List[int], List[List[int]]]]): Weights on connections. Defaults to ``None``.
|
||||
"""
|
||||
bad_connection_msg = (
|
||||
"The weight on connections between vertices and hyperedges must have the"
|
||||
" same size as the hyperedges."
|
||||
)
|
||||
if isinstance(e_list, tuple):
|
||||
e_list = list(e_list)
|
||||
if w_list is not None and isinstance(w_list, tuple):
|
||||
w_list = list(w_list)
|
||||
if isinstance(e_list[0], int) and w_list is None:
|
||||
w_list = [1] * len(e_list)
|
||||
e_list, w_list = [e_list], [w_list]
|
||||
elif isinstance(e_list[0], int) and w_list is not None:
|
||||
assert len(e_list) == len(w_list), bad_connection_msg
|
||||
e_list, w_list = [e_list], [w_list]
|
||||
elif isinstance(e_list[0], list) and w_list is None:
|
||||
w_list = [[1] * len(e) for e in e_list]
|
||||
assert len(e_list) == len(w_list), bad_connection_msg
|
||||
# TODO: this step can be speeded up
|
||||
for idx in range(len(e_list)):
|
||||
assert len(e_list[idx]) == len(w_list[idx]), bad_connection_msg
|
||||
cur_e, cur_w = np.array(e_list[idx]), np.array(w_list[idx])
|
||||
sorted_idx = np.argsort(cur_e)
|
||||
e_list[idx] = tuple(cur_e[sorted_idx].tolist())
|
||||
w_list[idx] = cur_w[sorted_idx].tolist()
|
||||
return e_list, w_list
|
||||
|
||||
def _fetch_H(self, direction: str, group_name: str):
|
||||
r"""Fetch the H matrix of the specified hyperedge group with ``torch.sparse_coo_tensor`` format.
|
||||
|
||||
Args:
|
||||
``direction`` (``str``): The direction of hyperedges can be either ``'v2e'`` or ``'e2v'``.
|
||||
``group_name`` (``str``): The name of the group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
assert direction in ["v2e", "e2v"], "direction must be one of ['v2e', 'e2v']"
|
||||
if direction == "v2e":
|
||||
select_idx = 0
|
||||
else:
|
||||
select_idx = 1
|
||||
num_e = len(self._raw_groups[group_name])
|
||||
e_idx, v_idx = [], []
|
||||
for _e_idx, e in enumerate(self._raw_groups[group_name].keys()):
|
||||
sub_e = e[select_idx]
|
||||
v_idx.extend(sub_e)
|
||||
e_idx.extend([_e_idx] * len(sub_e))
|
||||
|
||||
H = torch.sparse_coo_tensor(
|
||||
torch.tensor([v_idx, e_idx], dtype=torch.long),
|
||||
torch.ones(len(v_idx)),
|
||||
torch.Size([self.num_v, num_e]),
|
||||
device=self.device,
|
||||
).coalesce()
|
||||
return H
|
||||
|
||||
def _fetch_H_of_group(self, direction: str, group_name: str):
|
||||
r"""Fetch the H matrix of the specified hyperedge group with ``torch.sparse_coo_tensor`` format.
|
||||
|
||||
Args:
|
||||
``direction`` (``str``): The direction of hyperedges can be either ``'v2e'`` or ``'e2v'``.
|
||||
``group_name`` (``str``): The name of the group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
assert direction in ["v2e", "e2v"], "direction must be one of ['v2e', 'e2v']"
|
||||
if direction == "v2e":
|
||||
select_idx = 0
|
||||
else:
|
||||
select_idx = 1
|
||||
num_e = len(self._raw_groups[group_name])
|
||||
e_idx, v_idx = [], []
|
||||
for _e_idx, e in enumerate(self._raw_groups[group_name].keys()):
|
||||
sub_e = e[select_idx]
|
||||
v_idx.extend(sub_e)
|
||||
e_idx.extend([_e_idx] * len(sub_e))
|
||||
|
||||
H = torch.sparse_coo_tensor(
|
||||
torch.tensor([v_idx, e_idx], dtype=torch.long),
|
||||
torch.ones(len(v_idx)),
|
||||
torch.Size([self.num_v, num_e]),
|
||||
device=self.device,
|
||||
).coalesce()
|
||||
return H
|
||||
|
||||
def _fetch_R_of_group(self, direction: str, group_name: str):
|
||||
r"""Fetch the R matrix of the specified hyperedge group with ``torch.sparse_coo_tensor`` format.
|
||||
|
||||
Args:
|
||||
``direction`` (``str``): The direction of hyperedges can be either ``'v2e'`` or ``'e2v'``.
|
||||
``group_name`` (``str``): The name of the group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
assert direction in ["v2e", "e2v"], "direction must be one of ['v2e', 'e2v']"
|
||||
if direction == "v2e":
|
||||
select_idx = 0
|
||||
else:
|
||||
select_idx = 1
|
||||
num_e = len(self._raw_groups[group_name])
|
||||
e_idx, v_idx, w_list = [], [], []
|
||||
for _e_idx, e in enumerate(self._raw_groups[group_name].keys()):
|
||||
sub_e = e[select_idx]
|
||||
v_idx.extend(sub_e)
|
||||
e_idx.extend([_e_idx] * len(sub_e))
|
||||
w_list.extend(self._raw_groups[group_name][e][f"w_{direction}"])
|
||||
R = torch.sparse_coo_tensor(
|
||||
torch.vstack([v_idx, e_idx]),
|
||||
torch.tensor(w_list),
|
||||
torch.Size([self.num_v, num_e]),
|
||||
device=self.device,
|
||||
).coalesce()
|
||||
return R
|
||||
|
||||
def _fetch_W_of_group(self, group_name: str):
|
||||
r"""Fetch the W matrix of the specified hyperedge group with ``torch.sparse_coo_tensor`` format.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
w_list = [1.0] * len(self._raw_groups["main"])
|
||||
W = torch.tensor(w_list, device=self.device).view((-1, 1))
|
||||
return W
|
||||
|
||||
# some structure modification functions
|
||||
def add_hyperedges(
|
||||
self,
|
||||
e_list_v2e: Union[List[int], List[List[int]]],
|
||||
e_list_e2v: Union[List[int], List[List[int]]],
|
||||
w_list_v2e: Optional[Union[List[float], List[List[float]]]] = None,
|
||||
w_list_e2v: Optional[Union[List[float], List[List[float]]]] = None,
|
||||
e_weight: Optional[Union[float, List[float]]] = None,
|
||||
merge_op: str = "mean",
|
||||
group_name: str = "main",
|
||||
):
|
||||
r"""Add hyperedges to the hypergraph. If the ``group_name`` is not specified, the hyperedges will be added to the default ``main`` hyperedge group.
|
||||
|
||||
Args:
|
||||
``num_v`` (``int``): The number of vertices in the hypergraph.
|
||||
``e_list_v2e`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the vertices point to the hyperedges.
|
||||
``e_list_e2v`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the hyperedges point to the vertices.
|
||||
``w_list_v2e`` (``Union[List[float], List[List[float]]]``, optional): The weights are attached to the connections from vertices to hyperedges, which has the same shape
|
||||
as ``e_list_v2e``. If set to ``None``, the value ``1`` is used for all connections. Defaults to ``None``.
|
||||
``w_list_e2v`` (``Union[List[float], List[List[float]]]``, optional): The weights are attached to the connections from the hyperedges to the vertices, which has the
|
||||
same shape to ``e_list_e2v``. If set to ``None``, the value ``1`` is used for all connections. Defaults to ``None``.
|
||||
``e_weight`` (``Union[float, List[float]]``, optional): A list of weights for hyperedges. If set to ``None``, the value ``1`` is used for all hyperedges. Defaults to ``None``.
|
||||
``merge_op`` (``str``): The merge operation for the conflicting hyperedges. The possible values are ``mean``, ``sum``, ``max``, and ``min``. Defaults to ``mean``.
|
||||
``group_name`` (``str``, optional): The target hyperedge group to add these hyperedges. Defaults to the ``main`` hyperedge group.
|
||||
"""
|
||||
e_list_v2e, w_list_v2e = self._format_e_list_and_w_on_them(
|
||||
e_list_v2e, w_list_v2e
|
||||
)
|
||||
e_list_e2v, w_list_e2v = self._format_e_list_and_w_on_them(
|
||||
e_list_e2v, w_list_e2v
|
||||
)
|
||||
if e_weight is None:
|
||||
e_weight = [1.0] * len(e_list_v2e)
|
||||
assert len(e_list_v2e) == len(
|
||||
e_weight
|
||||
), "The number of hyperedges and the number of weights are not equal."
|
||||
assert len(e_list_v2e) == len(
|
||||
e_list_e2v
|
||||
), "Hyperedges of 'v2e' and 'e2v' must have the same size."
|
||||
for _idx in range(len(e_list_v2e)):
|
||||
self._add_hyperedge(
|
||||
self._hyperedge_code(e_list_v2e[_idx], e_list_e2v[_idx]),
|
||||
{
|
||||
"w_v2e": w_list_v2e[_idx],
|
||||
"w_e2v": w_list_e2v[_idx],
|
||||
"w_e": e_weight[_idx],
|
||||
},
|
||||
merge_op,
|
||||
group_name,
|
||||
)
|
||||
self._clear_cache(group_name)
|
||||
|
||||
def _add_hyperedge(
|
||||
self,
|
||||
hyperedge_code: Tuple[List[int], List[int]],
|
||||
content: Dict[str, Any],
|
||||
merge_op: str,
|
||||
group_name: str,
|
||||
):
|
||||
r"""Add a hyperedge to the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``hyperedge_code`` (``Tuple[List[int], List[int]]``): The hyperedge code.
|
||||
``content`` (``Dict[str, Any]``): The content of the hyperedge.
|
||||
``merge_op`` (``str``): The merge operation for the conflicting hyperedges.
|
||||
``group_name`` (``str``): The target hyperedge group to add this hyperedge.
|
||||
"""
|
||||
if group_name not in self._raw_groups:
|
||||
self._raw_groups[group_name] = {}
|
||||
self._raw_groups[group_name][hyperedge_code] = content
|
||||
else:
|
||||
if hyperedge_code not in self._raw_groups[group_name]:
|
||||
self._raw_groups[group_name][hyperedge_code] = content
|
||||
else:
|
||||
self._raw_groups[group_name][hyperedge_code] = self._merge_hyperedges(
|
||||
self._raw_groups[group_name][hyperedge_code], content, merge_op
|
||||
)
|
||||
|
||||
def remove_hyperedges(
|
||||
self,
|
||||
e_list_v2e: Union[List[int], List[List[int]]],
|
||||
e_list_e2v: Union[List[int], List[List[int]]],
|
||||
group_name: Optional[str] = None,
|
||||
):
|
||||
r"""Remove the specified hyperedges from the hypergraph.
|
||||
|
||||
Args:
|
||||
``e_list_v2e`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the vertices point to the hyperedges.
|
||||
``e_list_e2v`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the hyperedges point to the vertices.
|
||||
``group_name`` (``str``, optional): Remove these hyperedges from the specified hyperedge group. If not specified, the function will
|
||||
remove those hyperedges from all hyperedge groups. Defaults to the ``None``.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
assert len(e_list_v2e) == len(
|
||||
e_list_e2v
|
||||
), "Hyperedges of 'v2e' and 'e2v' must have the same size."
|
||||
e_list_v2e = self._format_e_list(e_list_v2e)
|
||||
e_list_e2v = self._format_e_list(e_list_e2v)
|
||||
if group_name is None:
|
||||
for _idx in range(len(e_list_v2e)):
|
||||
e_code = self._hyperedge_code(e_list_v2e[_idx], e_list_e2v[_idx])
|
||||
for name in self.group_names:
|
||||
self._raw_groups[name].pop(e_code, None)
|
||||
else:
|
||||
for _idx in range(len(e_list_v2e)):
|
||||
e_code = self._hyperedge_code(e_list_v2e[_idx], e_list_e2v[_idx])
|
||||
self._raw_groups[group_name].pop(e_code, None)
|
||||
self._clear_cache(group_name)
|
||||
|
||||
@abc.abstractmethod
|
||||
def drop_hyperedges(self, drop_rate: float, ord="uniform"):
|
||||
r"""Randomly drop hyperedges from the hypergraph. This function will return a new hypergraph with non-dropped hyperedges.
|
||||
|
||||
Args:
|
||||
``drop_rate`` (``float``): The drop rate of hyperedges.
|
||||
``ord`` (``str``): The order of dropping edges. Currently, only ``'uniform'`` is supported. Defaults to ``uniform``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def drop_hyperedges_of_group(
|
||||
self, group_name: str, drop_rate: float, ord="uniform"
|
||||
):
|
||||
r"""Randomly drop hyperedges from the specified hyperedge group. This function will return a new hypergraph with non-dropped hyperedges.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the hyperedge group.
|
||||
``drop_rate`` (``float``): The drop rate of hyperedges.
|
||||
``ord`` (``str``): The order of dropping edges. Currently, only ``'uniform'`` is supported. Defaults to ``uniform``.
|
||||
"""
|
||||
|
||||
# properties for the hypergraph
|
||||
@property
|
||||
def v(self) -> List[int]:
|
||||
r"""Return the list of vertices."""
|
||||
if self.cache.get("v") is None:
|
||||
self.cache["v"] = list(range(self.num_v))
|
||||
return self.cache["v"]
|
||||
|
||||
@property
|
||||
def v_weight(self) -> List[float]:
|
||||
r"""Return the vertex weights of the hypergraph."""
|
||||
if self._v_weight is None:
|
||||
self._v_weight = [1.0] * self.num_v
|
||||
return self._v_weight
|
||||
|
||||
@v_weight.setter
|
||||
def v_weight(self, v_weight: List[float]):
|
||||
r"""Set the vertex weights of the hypergraph."""
|
||||
assert (
|
||||
len(v_weight) == self.num_v
|
||||
), "The length of vertex weights must be equal to the number of vertices."
|
||||
self._v_weight = v_weight
|
||||
self._clear_cache()
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def e(self) -> Tuple[List[List[int]], List[float]]:
|
||||
r"""Return all hyperedges and weights in the hypergraph."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e_of_group(self, group_name: str) -> Tuple[List[List[int]], List[float]]:
|
||||
r"""Return all hyperedges and weights in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
|
||||
@property
|
||||
def v_property(self):
|
||||
return self._v_property
|
||||
|
||||
@property
|
||||
def e_property(self):
|
||||
group_e_property = {}
|
||||
for group in self._raw_groups:
|
||||
group_e_property[group] = list(self._raw_groups[group].values())
|
||||
return group_e_property
|
||||
|
||||
@property
|
||||
def num_v(self) -> int:
|
||||
r"""Return the number of vertices in the hypergraph."""
|
||||
return self._num_v
|
||||
|
||||
@property
|
||||
def num_e(self) -> int:
|
||||
r"""Return the number of hyperedges in the hypergraph."""
|
||||
_num_e = 0
|
||||
for name in self.group_names:
|
||||
_num_e += len(self._raw_groups[name])
|
||||
return _num_e
|
||||
|
||||
def num_e_of_group(self, group_name: str) -> int:
|
||||
r"""Return the number of hyperedges in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
return len(self._raw_groups[group_name])
|
||||
|
||||
@property
|
||||
def num_groups(self) -> int:
|
||||
r"""Return the number of hyperedge groups in the hypergraph."""
|
||||
return len(self._raw_groups)
|
||||
|
||||
@property
|
||||
def group_names(self) -> List[str]:
|
||||
r"""Return the names of hyperedge groups in the hypergraph."""
|
||||
return list(self._raw_groups.keys())
|
||||
|
||||
# properties for deep learning
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def vars_for_DL(self) -> List[str]:
|
||||
r"""Return a name list of available variables for deep learning in this type of hypergraph.
|
||||
"""
|
||||
|
||||
@property
|
||||
def W_v(self) -> torch.Tensor:
|
||||
r"""Return the vertex weight matrix of the hypergraph."""
|
||||
if self.cache["W_v"] is None:
|
||||
self.cache["W_v"] = torch.tensor(
|
||||
self.v_weight, dtype=torch.float, device=self.device
|
||||
).view(-1, 1)
|
||||
return self.cache["W_v"]
|
||||
|
||||
@property
|
||||
def W_e(self) -> torch.Tensor:
|
||||
r"""Return the hyperedge weight matrix of the hypergraph."""
|
||||
if self.cache["W_e"] is None:
|
||||
_tmp = [self.W_e_of_group(name) for name in self.group_names]
|
||||
self.cache["W_e"] = torch.cat(_tmp, dim=0)
|
||||
return self.cache["W_e"]
|
||||
|
||||
def W_e_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the hyperedge weight matrix of the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name]["W_e"] is None:
|
||||
self.group_cache[group_name]["W_e"] = self._fetch_W_of_group(group_name)
|
||||
return self.group_cache[group_name]["W_e"]
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def H(self) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix."""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def H_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
|
||||
@property
|
||||
def H_v2e(self) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix with ``sparse matrix`` format."""
|
||||
if self.cache.get("H_v2e") is None:
|
||||
_tmp = [self.H_v2e_of_group(name) for name in self.group_names]
|
||||
self.cache["H_v2e"] = torch.cat(_tmp, dim=1)
|
||||
return self.cache["H_v2e"]
|
||||
|
||||
def H_v2e_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix with ``sparse matrix`` format in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name].get("H_v2e") is None:
|
||||
self.group_cache[group_name]["H_v2e"] = self._fetch_H_of_group(
|
||||
"v2e", group_name
|
||||
)
|
||||
return self.group_cache[group_name]["H_v2e"]
|
||||
|
||||
@property
|
||||
def H_e2v(self) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix with ``sparse matrix`` format."""
|
||||
if self.cache.get("H_e2v") is None:
|
||||
_tmp = [self.H_e2v_of_group(name) for name in self.group_names]
|
||||
self.cache["H_e2v"] = torch.cat(_tmp, dim=1)
|
||||
return self.cache["H_e2v"]
|
||||
|
||||
def H_e2v_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix with ``sparse matrix`` format in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name].get("H_e2v") is None:
|
||||
self.group_cache[group_name]["H_e2v"] = self._fetch_H_of_group(
|
||||
"e2v", group_name
|
||||
)
|
||||
return self.group_cache[group_name]["H_e2v"]
|
||||
|
||||
@property
|
||||
def R_v2e(self) -> torch.Tensor:
|
||||
r"""Return the weight matrix of connections (vertices point to hyperedges) with ``sparse matrix`` format.
|
||||
"""
|
||||
if self.cache.get("R_v2e") is None:
|
||||
_tmp = [self.R_v2e_of_group(name) for name in self.group_names]
|
||||
self.cache["R_v2e"] = torch.cat(_tmp, dim=1)
|
||||
return self.cache["R_v2e"]
|
||||
|
||||
def R_v2e_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the weight matrix of connections (vertices point to hyperedges) with ``sparse matrix`` format in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name].get("R_v2e") is None:
|
||||
self.group_cache[group_name]["R_v2e"] = self._fetch_R_of_group(
|
||||
"v2e", group_name
|
||||
)
|
||||
return self.group_cache[group_name]["R_v2e"]
|
||||
|
||||
@property
|
||||
def R_e2v(self) -> torch.Tensor:
|
||||
r"""Return the weight matrix of connections (hyperedges point to vertices) with ``sparse matrix`` format.
|
||||
"""
|
||||
if self.cache.get("R_e2v") is None:
|
||||
_tmp = [self.R_e2v_of_group(name) for name in self.group_names]
|
||||
self.cache["R_e2v"] = torch.cat(_tmp, dim=1)
|
||||
return self.cache["R_e2v"]
|
||||
|
||||
def R_e2v_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the weight matrix of connections (hyperedges point to vertices) with ``sparse matrix`` format in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name].get("R_e2v") is None:
|
||||
self.group_cache[group_name]["R_e2v"] = self._fetch_R_of_group(
|
||||
"e2v", group_name
|
||||
)
|
||||
return self.group_cache[group_name]["R_e2v"]
|
||||
|
||||
# spectral-based smoothing
|
||||
def smoothing(self, X: torch.Tensor, L: torch.Tensor, lamb: float) -> torch.Tensor:
|
||||
r"""Spectral-based smoothing.
|
||||
|
||||
.. math::
|
||||
X_{smoothed} = X + \lambda \mathcal{L} X
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): The vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``L`` (``torch.Tensor``): The Laplacian matrix with ``torch.sparse_coo_tensor`` format. Size :math:`(|\mathcal{V}|, |\mathcal{V}|)`.
|
||||
``lamb`` (``float``): :math:`\lambda`, the strength of smoothing.
|
||||
"""
|
||||
return X + lamb * torch.sparse.mm(L, X)
|
||||
|
||||
# message passing functions
|
||||
@abc.abstractmethod
|
||||
def v2e_aggregation(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
drop_rate: float = 0.0,
|
||||
):
|
||||
r"""Message aggretation step of ``vertices to hyperedges``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e_aggregation_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
drop_rate: float = 0.0,
|
||||
):
|
||||
r"""Message aggregation step of ``vertices to hyperedges`` in specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e_update(self, X: torch.Tensor, e_weight: Optional[torch.Tensor] = None):
|
||||
r"""Message update step of ``vertices to hyperedges``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e_update_of_group(
|
||||
self, group_name: str, X: torch.Tensor, e_weight: Optional[torch.Tensor] = None
|
||||
):
|
||||
r"""Message update step of ``vertices to hyperedges`` in specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
e_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``vertices to hyperedges``. The combination of ``v2e_aggregation`` and ``v2e_update``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
e_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``vertices to hyperedges`` in specified hyperedge group. The combination of ``e2v_aggregation_of_group`` and ``e2v_update_of_group``.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_aggregation(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message aggregation step of ``hyperedges to vertices``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_aggregation_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message aggregation step of ``hyperedges to vertices`` in specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_update(self, X: torch.Tensor, v_weight: Optional[torch.Tensor] = None):
|
||||
r"""Message update step of ``hyperedges to vertices``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_update_of_group(
|
||||
self, group_name: str, X: torch.Tensor, v_weight: Optional[torch.Tensor] = None
|
||||
):
|
||||
r"""Message update step of ``hyperedges to vertices`` in specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``hyperedges to vertices``. The combination of ``e2v_aggregation`` and ``e2v_update``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``hyperedges to vertices`` in specified hyperedge group. The combination of ``e2v_aggregation_of_group`` and ``e2v_update_of_group``.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2v(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_aggr: Optional[str] = None,
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
e_weight: Optional[torch.Tensor] = None,
|
||||
e2v_aggr: Optional[str] = None,
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``vertices to vertices``. The combination of ``v2e`` and ``e2v``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, this ``aggr`` will be used to both ``v2e`` and ``e2v``.
|
||||
``v2e_aggr`` (``str``, optional): The aggregation method for hyperedges to vertices. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, it will override the ``aggr`` in ``e2v``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e2v_aggr`` (``str``, optional): The aggregation method for vertices to hyperedges. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, it will override the ``aggr`` in ``v2e``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2v_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_aggr: Optional[str] = None,
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
e_weight: Optional[torch.Tensor] = None,
|
||||
e2v_aggr: Optional[str] = None,
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``vertices to vertices`` in specified hyperedge group. The combination of ``v2e_of_group`` and ``e2v_of_group``.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, this ``aggr`` will be used to both ``v2e_of_group`` and ``e2v_of_group``.
|
||||
``v2e_aggr`` (``str``, optional): The aggregation method for hyperedges to vertices. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, it will override the ``aggr`` in ``e2v_of_group``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e2v_aggr`` (``str``, optional): The aggregation method for vertices to hyperedges. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, it will override the ``aggr`` in ``v2e_of_group``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,420 @@
|
||||
from copy import deepcopy
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
import easygraph as eg
|
||||
import easygraph.convert as convert
|
||||
|
||||
from easygraph.classes.directed_graph import DiGraph
|
||||
from easygraph.classes.multigraph import MultiGraph
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["MultiDiGraph"]
|
||||
|
||||
|
||||
class MultiDiGraph(MultiGraph, DiGraph):
|
||||
edge_key_dict_factory = dict
|
||||
|
||||
def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr):
|
||||
"""Initialize a graph with edges, name, or graph attributes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
incoming_graph_data : input graph
|
||||
Data to initialize graph. If incoming_graph_data=None (default)
|
||||
an empty graph is created. The data can be an edge list, or any
|
||||
EasyGraph graph object. If the corresponding optional Python
|
||||
packages are installed the data can also be a NumPy matrix
|
||||
or 2d ndarray, a SciPy sparse matrix, or a PyGraphviz graph.
|
||||
|
||||
multigraph_input : bool or None (default None)
|
||||
Note: Only used when `incoming_graph_data` is a dict.
|
||||
If True, `incoming_graph_data` is assumed to be a
|
||||
dict-of-dict-of-dict-of-dict structure keyed by
|
||||
node to neighbor to edge keys to edge data for multi-edges.
|
||||
A EasyGraphError is raised if this is not the case.
|
||||
If False, :func:`to_easygraph_graph` is used to try to determine
|
||||
the dict's graph data structure as either a dict-of-dict-of-dict
|
||||
keyed by node to neighbor to edge data, or a dict-of-iterable
|
||||
keyed by node to neighbors.
|
||||
If None, the treatment for True is tried, but if it fails,
|
||||
the treatment for False is tried.
|
||||
|
||||
attr : keyword arguments, optional (default= no attributes)
|
||||
Attributes to add to graph as key=value pairs.
|
||||
|
||||
See Also
|
||||
--------
|
||||
convert
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G = eg.Graph(name="my graph")
|
||||
>>> e = [(1, 2), (2, 3), (3, 4)] # list of edges
|
||||
>>> G = eg.Graph(e)
|
||||
|
||||
Arbitrary graph attribute pairs (key=value) may be assigned
|
||||
|
||||
>>> G = eg.Graph(e, day="Friday")
|
||||
>>> G.graph
|
||||
{'day': 'Friday'}
|
||||
|
||||
"""
|
||||
self.edge_key_dict_factory = self.edge_key_dict_factory
|
||||
# multigraph_input can be None/True/False. So check "is not False"
|
||||
if isinstance(incoming_graph_data, dict) and multigraph_input is not False:
|
||||
DiGraph.__init__(self)
|
||||
try:
|
||||
convert.from_dict_of_dicts(
|
||||
incoming_graph_data, create_using=self, multigraph_input=True
|
||||
)
|
||||
self.graph.update(attr)
|
||||
except Exception as err:
|
||||
if multigraph_input is True:
|
||||
raise EasyGraphError(
|
||||
f"converting multigraph_input raised:\n{type(err)}: {err}"
|
||||
)
|
||||
DiGraph.__init__(self, incoming_graph_data, **attr)
|
||||
else:
|
||||
DiGraph.__init__(self, incoming_graph_data, **attr)
|
||||
|
||||
def add_edge(self, u_for_edge, v_for_edge, key=None, **attr):
|
||||
"""Add an edge between u and v.
|
||||
|
||||
The nodes u and v will be automatically added if they are
|
||||
not already in the graph.
|
||||
|
||||
Edge attributes can be specified with keywords or by directly
|
||||
accessing the edge's attribute dictionary. See examples below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u_for_edge, v_for_edge : nodes
|
||||
Nodes can be, for example, strings or numbers.
|
||||
Nodes must be hashable (and not None) Python objects.
|
||||
key : hashable identifier, optional (default=lowest unused integer)
|
||||
Used to distinguish multiedges between a pair of nodes.
|
||||
attr : keyword arguments, optional
|
||||
Edge data (or labels or objects) can be assigned using
|
||||
keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The edge key assigned to the edge.
|
||||
|
||||
See Also
|
||||
--------
|
||||
add_edges_from : add a collection of edges
|
||||
|
||||
Notes
|
||||
-----
|
||||
To replace/update edge data, use the optional key argument
|
||||
to identify a unique edge. Otherwise a new edge will be created.
|
||||
|
||||
EasyGraph algorithms designed for weighted graphs cannot use
|
||||
multigraphs directly because it is not clear how to handle
|
||||
multiedge weights. Convert to Graph using edge attribute
|
||||
'weight' to enable weighted graph algorithms.
|
||||
|
||||
Default keys are generated using the method `new_edge_key()`.
|
||||
This method can be overridden by subclassing the base class and
|
||||
providing a custom `new_edge_key()` method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following all add the edge e=(1, 2) to graph G:
|
||||
|
||||
>>> G = eg.MultiDiGraph()
|
||||
>>> e = (1, 2)
|
||||
>>> key = G.add_edge(1, 2) # explicit two-node form
|
||||
>>> G.add_edge(*e) # single edge as tuple of two nodes
|
||||
1
|
||||
>>> G.add_edges_from([(1, 2)]) # add edges from iterable container
|
||||
[2]
|
||||
|
||||
Associate data to edges using keywords:
|
||||
|
||||
>>> key = G.add_edge(1, 2, weight=3)
|
||||
>>> key = G.add_edge(1, 2, key=0, weight=4) # update data for key=0
|
||||
>>> key = G.add_edge(1, 3, weight=7, capacity=15, length=342.7)
|
||||
|
||||
For non-string attribute keys, use subscript notation.
|
||||
|
||||
>>> ekey = G.add_edge(1, 2)
|
||||
>>> G[1][2][0].update({0: 5})
|
||||
>>> G.edges[1, 2, 0].update({0: 5})
|
||||
|
||||
>>>
|
||||
>>>
|
||||
"""
|
||||
u, v = u_for_edge, v_for_edge
|
||||
if "attr" in attr:
|
||||
temp = attr.get("attr")
|
||||
attr = temp if temp != None else {}
|
||||
# add nodes
|
||||
if u not in self._adj:
|
||||
if u is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
self._adj[u] = self.adjlist_inner_dict_factory()
|
||||
self._pred[u] = self.adjlist_inner_dict_factory()
|
||||
self._node[u] = self.node_attr_dict_factory()
|
||||
if v not in self._adj:
|
||||
if v is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
self._adj[v] = self.adjlist_inner_dict_factory()
|
||||
self._pred[v] = self.adjlist_inner_dict_factory()
|
||||
self._node[v] = self.node_attr_dict_factory()
|
||||
if key is None:
|
||||
key = self.new_edge_key(u, v)
|
||||
if v in self._adj[u]:
|
||||
keydict = self._adj[u][v]
|
||||
datadict = keydict.get(key, self.edge_key_dict_factory())
|
||||
datadict.update(attr)
|
||||
keydict[key] = datadict
|
||||
else:
|
||||
# selfloops work this way without special treatment
|
||||
datadict = self.edge_attr_dict_factory()
|
||||
datadict.update(attr)
|
||||
keydict = self.edge_key_dict_factory()
|
||||
keydict[key] = datadict
|
||||
self._adj[u][v] = keydict
|
||||
self._pred[v][u] = keydict
|
||||
return key
|
||||
|
||||
def remove_edge(self, u, v, key=None):
|
||||
"""Remove an edge between u and v.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
Remove an edge between nodes u and v.
|
||||
key : hashable identifier, optional (default=None)
|
||||
Used to distinguish multiple edges between a pair of nodes.
|
||||
If None remove a single (arbitrary) edge between u and v.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If there is not an edge between u and v, or
|
||||
if there is no edge with the specified key.
|
||||
|
||||
See Also
|
||||
--------
|
||||
remove_edges_from : remove a collection of edges
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.MultiDiGraph()
|
||||
>>> G.add_edges_from([(1, 2), (1, 2), (1, 2)]) # key_list returned
|
||||
[0, 1, 2]
|
||||
>>> G.remove_edge(1, 2) # remove a single (arbitrary) edge
|
||||
|
||||
For edges with keys
|
||||
|
||||
>>> G = eg.MultiDiGraph()
|
||||
>>> G.add_edge(1, 2, key="first")
|
||||
'first'
|
||||
>>> G.add_edge(1, 2, key="second")
|
||||
'second'
|
||||
>>> G.remove_edge(1, 2, key="second")
|
||||
|
||||
"""
|
||||
try:
|
||||
d = self._adj[u][v]
|
||||
except KeyError as err:
|
||||
raise EasyGraphError(f"The edge {u}-{v} is not in the graph.") from err
|
||||
# remove the edge with specified data
|
||||
if key is None:
|
||||
d.popitem()
|
||||
else:
|
||||
try:
|
||||
del d[key]
|
||||
except KeyError as err:
|
||||
msg = f"The edge {u}-{v} with key {key} is not in the graph."
|
||||
raise EasyGraphError(msg) from err
|
||||
if len(d) == 0:
|
||||
# remove the key entries if last edge
|
||||
del self._adj[u][v]
|
||||
del self._pred[v][u]
|
||||
|
||||
@property
|
||||
def edges(self):
|
||||
edges = list()
|
||||
for n, nbrs in self._adj.items():
|
||||
for nbr, kd in nbrs.items():
|
||||
for k, dd in kd.items():
|
||||
edges.append((n, nbr, k, dd))
|
||||
return edges
|
||||
|
||||
out_edges = edges
|
||||
|
||||
@property
|
||||
def in_edges(self):
|
||||
edges = list()
|
||||
for n, nbrs in self._adj.items():
|
||||
for nbr, kd in nbrs.items():
|
||||
for k, dd in kd.items():
|
||||
edges.append((nbr, n, k))
|
||||
return edges
|
||||
|
||||
@property
|
||||
def degree(self, weight="weight"):
|
||||
degree = dict()
|
||||
if weight is None:
|
||||
for n in self._node:
|
||||
succs = self._adj[n]
|
||||
preds = self._pred[n]
|
||||
deg = sum(len(keys) for keys in succs.values()) + sum(
|
||||
len(keys) for keys in preds.values()
|
||||
)
|
||||
degree[n] = deg
|
||||
else:
|
||||
for n in self._node:
|
||||
succs = self._adj[n]
|
||||
preds = self._pred[n]
|
||||
deg = sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in succs.values()
|
||||
for d in key_dict.values()
|
||||
) + sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in preds.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
degree[n] = deg
|
||||
|
||||
@property
|
||||
def in_degree(self, weight="weight"):
|
||||
degree = dict()
|
||||
if weight is None:
|
||||
for n in self._node:
|
||||
preds = self._pred[n]
|
||||
deg = sum(len(keys) for keys in preds.values())
|
||||
degree[n] = deg
|
||||
else:
|
||||
for n in self._node:
|
||||
preds = self._pred[n]
|
||||
deg = sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in preds.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
degree[n] = deg
|
||||
|
||||
@property
|
||||
def out_degree(self, weight="weight"):
|
||||
degree = dict()
|
||||
if weight is None:
|
||||
for n in self._node:
|
||||
succs = self._adj[n]
|
||||
deg = sum(len(keys) for keys in succs.values())
|
||||
degree[n] = deg
|
||||
else:
|
||||
for n in self._node:
|
||||
succs = self._adj[n]
|
||||
deg = sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in succs.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
degree[n] = deg
|
||||
|
||||
def is_multigraph(self):
|
||||
"""Returns True if graph is a multigraph, False otherwise."""
|
||||
return True
|
||||
|
||||
def is_directed(self):
|
||||
"""Returns True if graph is directed, False otherwise."""
|
||||
return True
|
||||
|
||||
def to_undirected(self, reciprocal=False):
|
||||
"""Returns an undirected representation of the multidigraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reciprocal : bool (optional)
|
||||
If True only keep edges that appear in both directions
|
||||
in the original digraph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : MultiGraph
|
||||
An undirected graph with the same name and nodes and
|
||||
with edge (u, v, data) if either (u, v, data) or (v, u, data)
|
||||
is in the digraph. If both edges exist in digraph and
|
||||
their edge data is different, only one edge is created
|
||||
with an arbitrary choice of which edge data to use.
|
||||
You must check and correct for this manually if desired.
|
||||
|
||||
See Also
|
||||
--------
|
||||
MultiGraph, add_edge, add_edges_from
|
||||
|
||||
Notes
|
||||
-----
|
||||
This returns a "deepcopy" of the edge, node, and
|
||||
graph attributes which attempts to completely copy
|
||||
all of the data and references.
|
||||
|
||||
This is in contrast to the similar D=MultiDiGraph(G) which
|
||||
returns a shallow copy of the data.
|
||||
|
||||
See the Python copy module for more information on shallow
|
||||
and deep copies, https://docs.python.org/3/library/copy.html.
|
||||
|
||||
Warning: If you have subclassed MultiDiGraph to use dict-like
|
||||
objects in the data structure, those changes do not transfer
|
||||
to the MultiGraph created by this method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(2) # or MultiGraph, etc
|
||||
>>> H = G.to_directed()
|
||||
>>> list(H.edges)
|
||||
[(0, 1), (1, 0)]
|
||||
>>> G2 = H.to_undirected()
|
||||
>>> list(G2.edges)
|
||||
[(0, 1)]
|
||||
"""
|
||||
G = eg.MultiGraph()
|
||||
G.graph.update(deepcopy(self.graph))
|
||||
G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
|
||||
if reciprocal is True:
|
||||
G.add_edges_from(
|
||||
(u, v, key, deepcopy(data))
|
||||
for u, nbrs in self._adj.items()
|
||||
for v, keydict in nbrs.items()
|
||||
for key, data in keydict.items()
|
||||
if v in self._pred[u] and key in self._pred[u][v]
|
||||
)
|
||||
else:
|
||||
G.add_edges_from(
|
||||
(u, v, key, deepcopy(data))
|
||||
for u, nbrs in self._adj.items()
|
||||
for v, keydict in nbrs.items()
|
||||
for key, data in keydict.items()
|
||||
)
|
||||
return G
|
||||
|
||||
def reverse(self, copy=True):
|
||||
"""Returns the reverse of the graph.
|
||||
|
||||
The reverse is a graph with the same nodes and edges
|
||||
but with the directions of the edges reversed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
copy : bool optional (default=True)
|
||||
If True, return a new DiGraph holding the reversed edges.
|
||||
If False, the reverse graph is created using a view of
|
||||
the original graph.
|
||||
"""
|
||||
if copy:
|
||||
H = self.__class__()
|
||||
H.graph.update(deepcopy(self.graph))
|
||||
H.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
|
||||
H.add_edges_from((v, u, k, deepcopy(d)) for u, v, k, d in self.edges)
|
||||
return H
|
||||
return eg.graphviews.reverse_view(self)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
from easygraph.utils import only_implemented_for_Directed_graph
|
||||
|
||||
|
||||
__all__ = ["reverse_view"]
|
||||
|
||||
|
||||
@only_implemented_for_Directed_graph
|
||||
def reverse_view(G):
|
||||
newG = G.__class__()
|
||||
newG._graph = G
|
||||
newG.graph = G.graph
|
||||
newG._node = G._node
|
||||
newG._succ, newG._pred = G._pred, G._succ
|
||||
newG._adj = newG._succ
|
||||
return newG
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,729 @@
|
||||
"""Base class for MultiGraph."""
|
||||
from copy import deepcopy
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
import easygraph as eg
|
||||
import easygraph.convert as convert
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["MultiGraph"]
|
||||
|
||||
|
||||
class MultiGraph(Graph):
|
||||
edge_key_dict_factory = dict
|
||||
|
||||
def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr):
|
||||
"""Initialize a graph with edges, name, or graph attributes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
incoming_graph_data : input graph
|
||||
Data to initialize graph. If incoming_graph_data=None (default)
|
||||
an empty graph is created. The data can be an edge list, or any
|
||||
EasyGraph graph object. If the corresponding optional Python
|
||||
packages are installed the data can also be a NumPy matrix
|
||||
or 2d ndarray, a SciPy sparse matrix, or a PyGraphviz graph.
|
||||
|
||||
multigraph_input : bool or None (default None)
|
||||
Note: Only used when `incoming_graph_data` is a dict.
|
||||
If True, `incoming_graph_data` is assumed to be a
|
||||
dict-of-dict-of-dict-of-dict structure keyed by
|
||||
node to neighbor to edge keys to edge data for multi-edges.
|
||||
A EasyGraphError is raised if this is not the case.
|
||||
If False, :func:`to_easygraph_graph` is used to try to determine
|
||||
the dict's graph data structure as either a dict-of-dict-of-dict
|
||||
keyed by node to neighbor to edge data, or a dict-of-iterable
|
||||
keyed by node to neighbors.
|
||||
If None, the treatment for True is tried, but if it fails,
|
||||
the treatment for False is tried.
|
||||
|
||||
attr : keyword arguments, optional (default= no attributes)
|
||||
Attributes to add to graph as key=value pairs.
|
||||
|
||||
See Also
|
||||
--------
|
||||
convert
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G = eg.Graph(name="my graph")
|
||||
>>> e = [(1, 2), (2, 3), (3, 4)] # list of edges
|
||||
>>> G = eg.Graph(e)
|
||||
|
||||
Arbitrary graph attribute pairs (key=value) may be assigned
|
||||
|
||||
>>> G = eg.Graph(e, day="Friday")
|
||||
>>> G.graph
|
||||
{'day': 'Friday'}
|
||||
|
||||
"""
|
||||
self.edge_key_dict_factory = self.edge_key_dict_factory
|
||||
if isinstance(incoming_graph_data, dict) and multigraph_input is not False:
|
||||
Graph.__init__(self)
|
||||
try:
|
||||
convert.from_dict_of_dicts(
|
||||
incoming_graph_data, create_using=self, multigraph_input=True
|
||||
)
|
||||
self.graph.update(attr)
|
||||
except Exception as err:
|
||||
if multigraph_input is True:
|
||||
raise eg.EasyGraphError(
|
||||
f"converting multigraph_input raised:\n{type(err)}: {err}"
|
||||
)
|
||||
Graph.__init__(self, incoming_graph_data, **attr)
|
||||
else:
|
||||
Graph.__init__(self, incoming_graph_data, **attr)
|
||||
|
||||
def new_edge_key(self, u, v):
|
||||
"""Returns an unused key for edges between nodes `u` and `v`.
|
||||
|
||||
The nodes `u` and `v` do not need to be already in the graph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
In the standard MultiGraph class the new key is the number of existing
|
||||
edges between `u` and `v` (increased if necessary to ensure unused).
|
||||
The first edge will have key 0, then 1, etc. If an edge is removed
|
||||
further new_edge_keys may not be in this order.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
|
||||
Returns
|
||||
-------
|
||||
key : int
|
||||
"""
|
||||
try:
|
||||
keydict = self._adj[u][v]
|
||||
except KeyError:
|
||||
return 0
|
||||
key = len(keydict)
|
||||
while key in keydict:
|
||||
key += 1
|
||||
return key
|
||||
|
||||
def add_edge(self, u_for_edge, v_for_edge, key=None, **attr):
|
||||
"""Add an edge between u and v.
|
||||
|
||||
The nodes u and v will be automatically added if they are
|
||||
not already in the graph.
|
||||
|
||||
Edge attributes can be specified with keywords or by directly
|
||||
accessing the edge's attribute dictionary. See examples below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u_for_edge, v_for_edge : nodes
|
||||
Nodes can be, for example, strings or numbers.
|
||||
Nodes must be hashable (and not None) Python objects.
|
||||
key : hashable identifier, optional (default=lowest unused integer)
|
||||
Used to distinguish multiedges between a pair of nodes.
|
||||
attr : keyword arguments, optional
|
||||
Edge data (or labels or objects) can be assigned using
|
||||
keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The edge key assigned to the edge.
|
||||
|
||||
See Also
|
||||
--------
|
||||
add_edges_from : add a collection of edges
|
||||
|
||||
Notes
|
||||
-----
|
||||
To replace/update edge data, use the optional key argument
|
||||
to identify a unique edge. Otherwise a new edge will be created.
|
||||
|
||||
EasyGraph algorithms designed for weighted graphs cannot use
|
||||
multigraphs directly because it is not clear how to handle
|
||||
multiedge weights. Convert to Graph using edge attribute
|
||||
'weight' to enable weighted graph algorithms.
|
||||
|
||||
Default keys are generated using the method `new_edge_key()`.
|
||||
This method can be overridden by subclassing the base class and
|
||||
providing a custom `new_edge_key()` method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following all add the edge e=(1, 2) to graph G:
|
||||
|
||||
>>> G = eg.MultiGraph()
|
||||
>>> e = (1, 2)
|
||||
>>> ekey = G.add_edge(1, 2) # explicit two-node form
|
||||
>>> G.add_edge(*e) # single edge as tuple of two nodes
|
||||
1
|
||||
>>> G.add_edges_from([(1, 2)]) # add edges from iterable container
|
||||
[2]
|
||||
|
||||
Associate data to edges using keywords:
|
||||
|
||||
>>> ekey = G.add_edge(1, 2, weight=3)
|
||||
>>> ekey = G.add_edge(1, 2, key=0, weight=4) # update data for key=0
|
||||
>>> ekey = G.add_edge(1, 3, weight=7, capacity=15, length=342.7)
|
||||
|
||||
For non-string attribute keys, use subscript notation.
|
||||
|
||||
>>> ekey = G.add_edge(1, 2)
|
||||
>>> G[1][2][0].update({0: 5})
|
||||
>>> G.edges[1, 2, 0].update({0: 5})
|
||||
"""
|
||||
u, v = u_for_edge, v_for_edge
|
||||
# add nodes
|
||||
if u not in self._adj:
|
||||
if u is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
self._adj[u] = self.adjlist_inner_dict_factory()
|
||||
self._node[u] = self.node_attr_dict_factory()
|
||||
if v not in self._adj:
|
||||
if v is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
self._adj[v] = self.adjlist_inner_dict_factory()
|
||||
self._node[v] = self.node_attr_dict_factory()
|
||||
if key is None:
|
||||
key = self.new_edge_key(u, v)
|
||||
if v in self._adj[u]:
|
||||
keydict = self._adj[u][v]
|
||||
datadict = keydict.get(key, self.edge_attr_dict_factory())
|
||||
datadict.update(attr)
|
||||
keydict[key] = datadict
|
||||
else:
|
||||
# selfloops work this way without special treatment
|
||||
datadict = self.edge_attr_dict_factory()
|
||||
datadict.update(attr)
|
||||
keydict = self.edge_key_dict_factory()
|
||||
keydict[key] = datadict
|
||||
self._adj[u][v] = keydict
|
||||
self._adj[v][u] = keydict
|
||||
return key
|
||||
|
||||
def add_edges_from(self, ebunch_to_add, **attr):
|
||||
"""Add all the edges in ebunch_to_add.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ebunch_to_add : container of edges
|
||||
Each edge given in the container will be added to the
|
||||
graph. The edges can be:
|
||||
|
||||
- 2-tuples (u, v) or
|
||||
- 3-tuples (u, v, d) for an edge data dict d, or
|
||||
- 3-tuples (u, v, k) for not iterable key k, or
|
||||
- 4-tuples (u, v, k, d) for an edge with data and key k
|
||||
|
||||
attr : keyword arguments, optional
|
||||
Edge data (or labels or objects) can be assigned using
|
||||
keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A list of edge keys assigned to the edges in `ebunch`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
add_edge : add a single edge
|
||||
add_weighted_edges_from : convenient way to add weighted edges
|
||||
|
||||
Notes
|
||||
-----
|
||||
Adding the same edge twice has no effect but any edge data
|
||||
will be updated when each duplicate edge is added.
|
||||
|
||||
Edge attributes specified in an ebunch take precedence over
|
||||
attributes specified via keyword arguments.
|
||||
|
||||
Default keys are generated using the method ``new_edge_key()``.
|
||||
This method can be overridden by subclassing the base class and
|
||||
providing a custom ``new_edge_key()`` method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G.add_edges_from([(0, 1), (1, 2)]) # using a list of edge tuples
|
||||
>>> e = zip(range(0, 3), range(1, 4))
|
||||
>>> G.add_edges_from(e) # Add the path graph 0-1-2-3
|
||||
|
||||
Associate data to edges
|
||||
|
||||
>>> G.add_edges_from([(1, 2), (2, 3)], weight=3)
|
||||
>>> G.add_edges_from([(3, 4), (1, 4)], label="WN2898")
|
||||
"""
|
||||
keylist = []
|
||||
for e in ebunch_to_add:
|
||||
ne = len(e)
|
||||
if ne == 4:
|
||||
u, v, key, dd = e
|
||||
elif ne == 3:
|
||||
u, v, dd = e
|
||||
key = None
|
||||
elif ne == 2:
|
||||
u, v = e
|
||||
dd = {}
|
||||
key = None
|
||||
else:
|
||||
msg = f"Edge tuple {e} must be a 2-tuple, 3-tuple or 4-tuple."
|
||||
raise EasyGraphError(msg)
|
||||
ddd = {}
|
||||
ddd.update(attr)
|
||||
try:
|
||||
ddd.update(dd)
|
||||
except (TypeError, ValueError):
|
||||
if ne != 3:
|
||||
raise
|
||||
key = dd # ne == 3 with 3rd value not dict, must be a key
|
||||
key = self.add_edge(u, v, key)
|
||||
self[u][v][key].update(ddd)
|
||||
keylist.append(key)
|
||||
return keylist
|
||||
|
||||
def remove_edge(self, u, v, key=None):
|
||||
"""Remove an edge between u and v.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
Remove an edge between nodes u and v.
|
||||
key : hashable identifier, optional (default=None)
|
||||
Used to distinguish multiple edges between a pair of nodes.
|
||||
If None remove a single (arbitrary) edge between u and v.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If there is not an edge between u and v, or
|
||||
if there is no edge with the specified key.
|
||||
|
||||
See Also
|
||||
--------
|
||||
remove_edges_from : remove a collection of edges
|
||||
|
||||
Examples
|
||||
--------
|
||||
For multiple edges
|
||||
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph, etc
|
||||
>>> G.add_edges_from([(1, 2), (1, 2), (1, 2)]) # key_list returned
|
||||
[0, 1, 2]
|
||||
>>> G.remove_edge(1, 2) # remove a single (arbitrary) edge
|
||||
|
||||
For edges with keys
|
||||
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph, etc
|
||||
>>> G.add_edge(1, 2, key="first")
|
||||
'first'
|
||||
>>> G.add_edge(1, 2, key="second")
|
||||
'second'
|
||||
>>> G.remove_edge(1, 2, key="second")
|
||||
|
||||
"""
|
||||
try:
|
||||
d = self._adj[u][v]
|
||||
except KeyError as err:
|
||||
raise EasyGraphError(f"The edge {u}-{v} is not in the graph.") from err
|
||||
# remove the edge with specified data
|
||||
if key is None:
|
||||
d.popitem()
|
||||
else:
|
||||
try:
|
||||
del d[key]
|
||||
except KeyError as err:
|
||||
msg = f"The edge {u}-{v} with key {key} is not in the graph."
|
||||
raise EasyGraphError(msg) from err
|
||||
if len(d) == 0:
|
||||
# remove the key entries if last edge
|
||||
del self._adj[u][v]
|
||||
if u != v: # check for selfloop
|
||||
del self._adj[v][u]
|
||||
|
||||
def remove_edges_from(self, ebunch):
|
||||
"""Remove all edges specified in ebunch.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ebunch: list or container of edge tuples
|
||||
Each edge given in the list or container will be removed
|
||||
from the graph. The edges can be:
|
||||
|
||||
- 2-tuples (u, v) All edges between u and v are removed.
|
||||
- 3-tuples (u, v, key) The edge identified by key is removed.
|
||||
- 4-tuples (u, v, key, data) where data is ignored.
|
||||
|
||||
See Also
|
||||
--------
|
||||
remove_edge : remove a single edge
|
||||
|
||||
Notes
|
||||
-----
|
||||
Will fail silently if an edge in ebunch is not in the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Removing multiple copies of edges
|
||||
|
||||
>>> G = eg.MultiGraph()
|
||||
>>> keys = G.add_edges_from([(1, 2), (1, 2), (1, 2)])
|
||||
>>> G.remove_edges_from([(1, 2), (1, 2)])
|
||||
>>> list(G.edges())
|
||||
[(1, 2)]
|
||||
>>> G.remove_edges_from([(1, 2), (1, 2)]) # silently ignore extra copy
|
||||
>>> list(G.edges) # now empty graph
|
||||
[]
|
||||
"""
|
||||
for e in ebunch:
|
||||
try:
|
||||
self.remove_edge(*e[:3])
|
||||
except EasyGraphError:
|
||||
pass
|
||||
|
||||
def has_edge(self, u, v, key=None):
|
||||
"""Returns True if the graph has an edge between nodes u and v.
|
||||
|
||||
This is the same as `v in G[u] or key in G[u][v]`
|
||||
without KeyError exceptions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
Nodes can be, for example, strings or numbers.
|
||||
|
||||
key : hashable identifier, optional (default=None)
|
||||
If specified return True only if the edge with
|
||||
key is found.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edge_ind : bool
|
||||
True if edge is in the graph, False otherwise.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Can be called either using two nodes u, v, an edge tuple (u, v),
|
||||
or an edge tuple (u, v, key).
|
||||
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph
|
||||
>>> G = eg.complete_graph(4, create_using=eg.MultiDiGraph)
|
||||
>>> G.has_edge(0, 1) # using two nodes
|
||||
True
|
||||
>>> e = (0, 1)
|
||||
>>> G.has_edge(*e) # e is a 2-tuple (u, v)
|
||||
True
|
||||
>>> G.add_edge(0, 1, key="a")
|
||||
'a'
|
||||
>>> G.has_edge(0, 1, key="a") # specify key
|
||||
True
|
||||
>>> e = (0, 1, "a")
|
||||
>>> G.has_edge(*e) # e is a 3-tuple (u, v, 'a')
|
||||
True
|
||||
|
||||
The following syntax are equivalent:
|
||||
|
||||
>>> G.has_edge(0, 1)
|
||||
True
|
||||
>>> 1 in G[0] # though this gives :exc:`KeyError` if 0 not in G
|
||||
True
|
||||
|
||||
"""
|
||||
try:
|
||||
if key is None:
|
||||
return v in self._adj[u]
|
||||
else:
|
||||
return key in self._adj[u][v]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def edges(self):
|
||||
edges = list()
|
||||
seen = {}
|
||||
for n, nbrs in self._adj.items():
|
||||
for nbr, kd in nbrs.items():
|
||||
if nbr not in seen:
|
||||
for k, dd in kd.items():
|
||||
edges.append((n, nbr, k, dd))
|
||||
seen[n] = 1
|
||||
del seen
|
||||
return edges
|
||||
|
||||
def get_edge_data(self, u, v, key=None, default=None):
|
||||
"""Returns the attribute dictionary associated with edge (u, v).
|
||||
|
||||
This is identical to `G[u][v][key]` except the default is returned
|
||||
instead of an exception is the edge doesn't exist.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
|
||||
default : any Python object (default=None)
|
||||
Value to return if the edge (u, v) is not found.
|
||||
|
||||
key : hashable identifier, optional (default=None)
|
||||
Return data only for the edge with specified key.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edge_dict : dictionary
|
||||
The edge attribute dictionary.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph
|
||||
>>> key = G.add_edge(0, 1, key="a", weight=7)
|
||||
>>> G[0][1]["a"] # key='a'
|
||||
{'weight': 7}
|
||||
>>> G.edges[0, 1, "a"] # key='a'
|
||||
{'weight': 7}
|
||||
|
||||
Warning: we protect the graph data structure by making
|
||||
`G.edges` and `G[1][2]` read-only dict-like structures.
|
||||
However, you can assign values to attributes in e.g.
|
||||
`G.edges[1, 2, 'a']` or `G[1][2]['a']` using an additional
|
||||
bracket as shown next. You need to specify all edge info
|
||||
to assign to the edge data associated with an edge.
|
||||
|
||||
>>> G[0][1]["a"]["weight"] = 10
|
||||
>>> G.edges[0, 1, "a"]["weight"] = 10
|
||||
>>> G[0][1]["a"]["weight"]
|
||||
10
|
||||
>>> G.edges[1, 0, "a"]["weight"]
|
||||
10
|
||||
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph
|
||||
>>> G = eg.complete_graph(4, create_using=eg.MultiDiGraph)
|
||||
>>> G.get_edge_data(0, 1)
|
||||
{0: {}}
|
||||
>>> e = (0, 1)
|
||||
>>> G.get_edge_data(*e) # tuple form
|
||||
{0: {}}
|
||||
>>> G.get_edge_data("a", "b", default=0) # edge not in graph, return 0
|
||||
0
|
||||
"""
|
||||
try:
|
||||
if key is None:
|
||||
return self._adj[u][v]
|
||||
else:
|
||||
return self._adj[u][v][key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
@property
|
||||
def degree(self, weight="weight"):
|
||||
degree = dict()
|
||||
if weight is None:
|
||||
for n in self._nodes:
|
||||
nbrs = self._succ[n]
|
||||
deg = sum(len(keys) for keys in nbrs.values()) + (
|
||||
n in nbrs and len(nbrs[n])
|
||||
)
|
||||
degree[n] = deg
|
||||
else:
|
||||
for n in self._nodes:
|
||||
nbrs = self._succ[n]
|
||||
deg = sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in nbrs.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
if n in nbrs:
|
||||
deg += sum(d.get(weight, 1) for d in nbrs[n].values())
|
||||
degree[n] = deg
|
||||
|
||||
def is_multigraph(self):
|
||||
"""Returns True if graph is a multigraph, False otherwise."""
|
||||
return True
|
||||
|
||||
def is_directed(self):
|
||||
"""Returns True if graph is directed, False otherwise."""
|
||||
return False
|
||||
|
||||
def copy(self):
|
||||
"""Returns a copy of the graph.
|
||||
|
||||
The copy method by default returns an independent shallow copy
|
||||
of the graph and attributes. That is, if an attribute is a
|
||||
container, that container is shared by the original an the copy.
|
||||
Use Python's `copy.deepcopy` for new containers.
|
||||
|
||||
Notes
|
||||
-----
|
||||
All copies reproduce the graph structure, but data attributes
|
||||
may be handled in different ways. There are four types of copies
|
||||
of a graph that people might want.
|
||||
|
||||
Deepcopy -- A "deepcopy" copies the graph structure as well as
|
||||
all data attributes and any objects they might contain.
|
||||
The entire graph object is new so that changes in the copy
|
||||
do not affect the original object. (see Python's copy.deepcopy)
|
||||
|
||||
Data Reference (Shallow) -- For a shallow copy the graph structure
|
||||
is copied but the edge, node and graph attribute dicts are
|
||||
references to those in the original graph. This saves
|
||||
time and memory but could cause confusion if you change an attribute
|
||||
in one graph and it changes the attribute in the other.
|
||||
EasyGraph does not provide this level of shallow copy.
|
||||
|
||||
Independent Shallow -- This copy creates new independent attribute
|
||||
dicts and then does a shallow copy of the attributes. That is, any
|
||||
attributes that are containers are shared between the new graph
|
||||
and the original. This is exactly what `dict.copy()` provides.
|
||||
You can obtain this style copy using:
|
||||
|
||||
>>> G = eg.path_graph(5)
|
||||
>>> H = G.copy()
|
||||
>>> H = eg.Graph(G)
|
||||
>>> H = G.__class__(G)
|
||||
|
||||
Fresh Data -- For fresh data, the graph structure is copied while
|
||||
new empty data attribute dicts are created. The resulting graph
|
||||
is independent of the original and it has no edge, node or graph
|
||||
attributes. Fresh copies are not enabled. Instead use:
|
||||
|
||||
>>> H = G.__class__()
|
||||
>>> H.add_nodes_from(G)
|
||||
>>> H.add_edges_from(G.edges)
|
||||
|
||||
See the Python copy module for more information on shallow
|
||||
and deep copies, https://docs.python.org/3/library/copy.html.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : Graph
|
||||
A copy of the graph.
|
||||
|
||||
See Also
|
||||
--------
|
||||
to_directed: return a directed copy of the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> H = G.copy()
|
||||
|
||||
"""
|
||||
G = self.__class__()
|
||||
G.graph.update(self.graph)
|
||||
G.add_nodes_from((n, d.copy()) for n, d in self._node.items())
|
||||
G.add_edges_from(
|
||||
(u, v, key, datadict.copy())
|
||||
for u, nbrs in self._adj.items()
|
||||
for v, keydict in nbrs.items()
|
||||
for key, datadict in keydict.items()
|
||||
)
|
||||
return G
|
||||
|
||||
def to_directed(self):
|
||||
"""Returns a directed representation of the graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : MultiDiGraph
|
||||
A directed graph with the same name, same nodes, and with
|
||||
each edge (u, v, data) replaced by two directed edges
|
||||
(u, v, data) and (v, u, data).
|
||||
|
||||
Notes
|
||||
-----
|
||||
This returns a "deepcopy" of the edge, node, and
|
||||
graph attributes which attempts to completely copy
|
||||
all of the data and references.
|
||||
|
||||
This is in contrast to the similar D=DiGraph(G) which returns a
|
||||
shallow copy of the data.
|
||||
|
||||
See the Python copy module for more information on shallow
|
||||
and deep copies, https://docs.python.org/3/library/copy.html.
|
||||
|
||||
Warning: If you have subclassed MultiGraph to use dict-like objects
|
||||
in the data structure, those changes do not transfer to the
|
||||
MultiDiGraph created by this method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or MultiGraph, etc
|
||||
>>> G.add_edge(0, 1)
|
||||
>>> H = G.to_directed()
|
||||
>>> list(H.edges)
|
||||
[(0, 1), (1, 0)]
|
||||
|
||||
If already directed, return a (deep) copy
|
||||
|
||||
>>> G = eg.DiGraph() # or MultiDiGraph, etc
|
||||
>>> G.add_edge(0, 1)
|
||||
>>> H = G.to_directed()
|
||||
>>> list(H.edges)
|
||||
[(0, 1)]
|
||||
"""
|
||||
G = eg.MultiDiGraph()
|
||||
G.graph.update(deepcopy(self.graph))
|
||||
G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
|
||||
G.add_edges_from(
|
||||
(u, v, key, deepcopy(datadict))
|
||||
for u, nbrs in self.adj.items()
|
||||
for v, keydict in nbrs.items()
|
||||
for key, datadict in keydict.items()
|
||||
)
|
||||
return G
|
||||
|
||||
def number_of_edges(self, u=None, v=None):
|
||||
"""Returns the number of edges between two nodes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes, optional (Gefault=all edges)
|
||||
If u and v are specified, return the number of edges between
|
||||
u and v. Otherwise return the total number of all edges.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nedges : int
|
||||
The number of edges in the graph. If nodes `u` and `v` are
|
||||
specified return the number of edges between those nodes. If
|
||||
the graph is directed, this only returns the number of edges
|
||||
from `u` to `v`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
size
|
||||
|
||||
Examples
|
||||
--------
|
||||
For undirected multigraphs, this method counts the total number
|
||||
of edges in the graph::
|
||||
|
||||
>>> G = eg.MultiGraph()
|
||||
>>> G.add_edges_from([(0, 1), (0, 1), (1, 2)])
|
||||
[0, 1, 0]
|
||||
>>> G.number_of_edges()
|
||||
3
|
||||
|
||||
If you specify two nodes, this counts the total number of edges
|
||||
joining the two nodes::
|
||||
|
||||
>>> G.number_of_edges(0, 1)
|
||||
2
|
||||
|
||||
For directed multigraphs, this method can count the total number
|
||||
of directed edges from `u` to `v`::
|
||||
|
||||
>>> G = eg.MultiDiGraph()
|
||||
>>> G.add_edges_from([(0, 1), (0, 1), (1, 0)])
|
||||
[0, 1, 0]
|
||||
>>> G.number_of_edges(0, 1)
|
||||
2
|
||||
>>> G.number_of_edges(1, 0)
|
||||
1
|
||||
|
||||
"""
|
||||
if u is None:
|
||||
return self.size()
|
||||
try:
|
||||
edgedata = self._adj[u][v]
|
||||
except KeyError:
|
||||
return 0 # no such edge
|
||||
return len(edgedata)
|
||||
@@ -0,0 +1,447 @@
|
||||
from itertools import chain
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"set_edge_attributes",
|
||||
"add_path",
|
||||
"set_node_attributes",
|
||||
"selfloop_edges",
|
||||
"topological_sort",
|
||||
"number_of_selfloops",
|
||||
"density",
|
||||
]
|
||||
|
||||
|
||||
def set_edge_attributes(G, values, name=None):
|
||||
"""Sets edge attributes from a given value or dictionary of values.
|
||||
|
||||
.. Warning:: The call order of arguments `values` and `name`
|
||||
switched between v1.x & v2.x.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
|
||||
values : scalar value, dict-like
|
||||
What the edge attribute should be set to. If `values` is
|
||||
not a dictionary, then it is treated as a single attribute value
|
||||
that is then applied to every edge in `G`. This means that if
|
||||
you provide a mutable object, like a list, updates to that object
|
||||
will be reflected in the edge attribute for each edge. The attribute
|
||||
name will be `name`.
|
||||
|
||||
If `values` is a dict or a dict of dict, it should be keyed
|
||||
by edge tuple to either an attribute value or a dict of attribute
|
||||
key/value pairs used to update the edge's attributes.
|
||||
For multigraphs, the edge tuples must be of the form ``(u, v, key)``,
|
||||
where `u` and `v` are nodes and `key` is the edge key.
|
||||
For non-multigraphs, the keys must be tuples of the form ``(u, v)``.
|
||||
|
||||
name : string (optional, default=None)
|
||||
Name of the edge attribute to set if values is a scalar.
|
||||
|
||||
Examples
|
||||
--------
|
||||
After computing some property of the edges of a graph, you may want
|
||||
to assign a edge attribute to store the value of that property for
|
||||
each edge::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> bb = eg.edge_betweenness_centrality(G, normalized=False)
|
||||
>>> eg.set_edge_attributes(G, bb, "betweenness")
|
||||
>>> G.edges[1, 2]["betweenness"]
|
||||
2.0
|
||||
|
||||
If you provide a list as the second argument, updates to the list
|
||||
will be reflected in the edge attribute for each edge::
|
||||
|
||||
>>> labels = []
|
||||
>>> eg.set_edge_attributes(G, labels, "labels")
|
||||
>>> labels.append("foo")
|
||||
>>> G.edges[0, 1]["labels"]
|
||||
['foo']
|
||||
>>> G.edges[1, 2]["labels"]
|
||||
['foo']
|
||||
|
||||
If you provide a dictionary of dictionaries as the second argument,
|
||||
the entire dictionary will be used to update edge attributes::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> attrs = {(0, 1): {"attr1": 20, "attr2": "nothing"}, (1, 2): {"attr2": 3}}
|
||||
>>> eg.set_edge_attributes(G, attrs)
|
||||
>>> G[0][1]["attr1"]
|
||||
20
|
||||
>>> G[0][1]["attr2"]
|
||||
'nothing'
|
||||
>>> G[1][2]["attr2"]
|
||||
3
|
||||
|
||||
Note that if the dict contains edges that are not in `G`, they are
|
||||
silently ignored::
|
||||
|
||||
>>> G = eg.Graph([(0, 1)])
|
||||
>>> eg.set_edge_attributes(G, {(1, 2): {"weight": 2.0}})
|
||||
>>> (1, 2) in G.edges()
|
||||
False
|
||||
|
||||
"""
|
||||
if name is not None:
|
||||
# `values` does not contain attribute names
|
||||
try:
|
||||
# if `values` is a dict using `.items()` => {edge: value}
|
||||
if G.is_multigraph():
|
||||
for (u, v, key), value in values.items():
|
||||
try:
|
||||
G[u][v][key][name] = value
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for (u, v), value in values.items():
|
||||
try:
|
||||
G[u][v][name] = value
|
||||
except KeyError:
|
||||
pass
|
||||
except AttributeError:
|
||||
# treat `values` as a constant
|
||||
for u, v, data in G.edges:
|
||||
data[name] = values
|
||||
else:
|
||||
# `values` consists of doct-of-dict {edge: {attr: value}} shape
|
||||
if G.is_multigraph():
|
||||
for (u, v, key), d in values.items():
|
||||
try:
|
||||
G[u][v][key].update(d)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for (u, v), d in values.items():
|
||||
try:
|
||||
G[u][v].update(d)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def add_path(G_to_add_to, nodes_for_path, **attr):
|
||||
"""Add a path to the Graph G_to_add_to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G_to_add_to : graph
|
||||
A EasyGraph graph
|
||||
nodes_for_path : iterable container
|
||||
A container of nodes. A path will be constructed from
|
||||
the nodes (in order) and added to the graph.
|
||||
attr : keyword arguments, optional (default= no attributes)
|
||||
Attributes to add to every edge in path.
|
||||
|
||||
See Also
|
||||
--------
|
||||
add_star, add_cycle
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph()
|
||||
>>> eg.add_path(G, [0, 1, 2, 3])
|
||||
>>> eg.add_path(G, [10, 11, 12], weight=7)
|
||||
"""
|
||||
nlist = iter(nodes_for_path)
|
||||
try:
|
||||
first_node = next(nlist)
|
||||
except StopIteration:
|
||||
return
|
||||
G_to_add_to.add_node(first_node)
|
||||
G_to_add_to.add_edges_from(pairwise(chain((first_node,), nlist)), **attr)
|
||||
|
||||
|
||||
def set_node_attributes(G, values, name=None):
|
||||
"""Sets node attributes from a given value or dictionary of values.
|
||||
|
||||
.. Warning:: The call order of arguments `values` and `name`
|
||||
switched between v1.x & v2.x.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
|
||||
values : scalar value, dict-like
|
||||
What the node attribute should be set to. If `values` is
|
||||
not a dictionary, then it is treated as a single attribute value
|
||||
that is then applied to every node in `G`. This means that if
|
||||
you provide a mutable object, like a list, updates to that object
|
||||
will be reflected in the node attribute for every node.
|
||||
The attribute name will be `name`.
|
||||
|
||||
If `values` is a dict or a dict of dict, it should be keyed
|
||||
by node to either an attribute value or a dict of attribute key/value
|
||||
pairs used to update the node's attributes.
|
||||
|
||||
name : string (optional, default=None)
|
||||
Name of the node attribute to set if values is a scalar.
|
||||
|
||||
Examples
|
||||
--------
|
||||
After computing some property of the nodes of a graph, you may want
|
||||
to assign a node attribute to store the value of that property for
|
||||
each node::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> bb = eg.betweenness_centrality(G)
|
||||
>>> isinstance(bb, dict)
|
||||
True
|
||||
>>> eg.set_node_attributes(G, bb, "betweenness")
|
||||
>>> G.nodes[1]["betweenness"]
|
||||
1.0
|
||||
|
||||
If you provide a list as the second argument, updates to the list
|
||||
will be reflected in the node attribute for each node::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> labels = []
|
||||
>>> eg.set_node_attributes(G, labels, "labels")
|
||||
>>> labels.append("foo")
|
||||
>>> G.nodes[0]["labels"]
|
||||
['foo']
|
||||
>>> G.nodes[1]["labels"]
|
||||
['foo']
|
||||
>>> G.nodes[2]["labels"]
|
||||
['foo']
|
||||
|
||||
If you provide a dictionary of dictionaries as the second argument,
|
||||
the outer dictionary is assumed to be keyed by node to an inner
|
||||
dictionary of node attributes for that node::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> attrs = {0: {"attr1": 20, "attr2": "nothing"}, 1: {"attr2": 3}}
|
||||
>>> eg.set_node_attributes(G, attrs)
|
||||
>>> G.nodes[0]["attr1"]
|
||||
20
|
||||
>>> G.nodes[0]["attr2"]
|
||||
'nothing'
|
||||
>>> G.nodes[1]["attr2"]
|
||||
3
|
||||
>>> G.nodes[2]
|
||||
{}
|
||||
|
||||
Note that if the dictionary contains nodes that are not in `G`, the
|
||||
values are silently ignored::
|
||||
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_node(0)
|
||||
>>> eg.set_node_attributes(G, {0: "red", 1: "blue"}, name="color")
|
||||
>>> G.nodes[0]["color"]
|
||||
'red'
|
||||
>>> 1 in G.nodes
|
||||
False
|
||||
|
||||
"""
|
||||
# Set node attributes based on type of `values`
|
||||
if name is not None: # `values` must not be a dict of dict
|
||||
try: # `values` is a dict
|
||||
for n, v in values.items():
|
||||
try:
|
||||
G.nodes[n][name] = values[n]
|
||||
except KeyError:
|
||||
pass
|
||||
except AttributeError: # `values` is a constant
|
||||
for n in G:
|
||||
G.nodes[n][name] = values
|
||||
else: # `values` must be dict of dict
|
||||
for n, d in values.items():
|
||||
try:
|
||||
G.nodes[n].update(d)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def topological_generations(G):
|
||||
if not G.is_directed():
|
||||
raise AssertionError("Topological sort not defined on undirected graphs.")
|
||||
indegree_map = {v: d for v, d in G.in_degree().items() if d > 0}
|
||||
zero_indegree = [v for v, d in G.in_degree().items() if d == 0]
|
||||
while zero_indegree:
|
||||
this_generation = zero_indegree
|
||||
zero_indegree = []
|
||||
for node in this_generation:
|
||||
if node not in G:
|
||||
raise RuntimeError("Graph changed during iteration")
|
||||
for child in G.neighbors(node):
|
||||
try:
|
||||
indegree_map[child] -= 1
|
||||
except KeyError as err:
|
||||
raise RuntimeError("Graph changed during iteration") from err
|
||||
if indegree_map[child] == 0:
|
||||
zero_indegree.append(child)
|
||||
del indegree_map[child]
|
||||
yield this_generation
|
||||
|
||||
if indegree_map:
|
||||
raise AssertionError("Graph contains a cycle or graph changed during iteration")
|
||||
|
||||
|
||||
def topological_sort(G):
|
||||
for generation in topological_generations(G):
|
||||
yield from generation
|
||||
|
||||
|
||||
def number_of_selfloops(G):
|
||||
"""Returns the number of selfloop edges.
|
||||
|
||||
A selfloop edge has the same node at both ends.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nloops : int
|
||||
The number of selfloops.
|
||||
|
||||
See Also
|
||||
--------
|
||||
nodes_with_selfloops, selfloop_edges
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G.add_edge(1, 1)
|
||||
>>> G.add_edge(1, 2)
|
||||
>>> eg.number_of_selfloops(G)
|
||||
1
|
||||
"""
|
||||
return sum(1 for _ in eg.selfloop_edges(G))
|
||||
|
||||
|
||||
def selfloop_edges(G, data=False, keys=False, default=None):
|
||||
"""Returns an iterator over selfloop edges.
|
||||
|
||||
A selfloop edge has the same node at both ends.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph.
|
||||
data : string or bool, optional (default=False)
|
||||
Return selfloop edges as two tuples (u, v) (data=False)
|
||||
or three-tuples (u, v, datadict) (data=True)
|
||||
or three-tuples (u, v, datavalue) (data='attrname')
|
||||
keys : bool, optional (default=False)
|
||||
If True, return edge keys with each edge.
|
||||
default : value, optional (default=None)
|
||||
Value used for edges that don't have the requested attribute.
|
||||
Only relevant if data is not True or False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edgeiter : iterator over edge tuples
|
||||
An iterator over all selfloop edges.
|
||||
|
||||
See Also
|
||||
--------
|
||||
nodes_with_selfloops, number_of_selfloops
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.MultiGraph() # or Graph, DiGraph, MultiDiGraph, etc
|
||||
>>> ekey = G.add_edge(1, 1)
|
||||
>>> ekey = G.add_edge(1, 2)
|
||||
>>> list(eg.selfloop_edges(G))
|
||||
[(1, 1)]
|
||||
>>> list(eg.selfloop_edges(G, data=True))
|
||||
[(1, 1, {})]
|
||||
>>> list(eg.selfloop_edges(G, keys=True))
|
||||
[(1, 1, 0)]
|
||||
>>> list(eg.selfloop_edges(G, keys=True, data=True))
|
||||
[(1, 1, 0, {})]
|
||||
"""
|
||||
if data is True:
|
||||
if G.is_multigraph():
|
||||
if keys is True:
|
||||
return (
|
||||
(n, n, k, d)
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for k, d in nbrs[n].items()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(n, n, d)
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for d in nbrs[n].values()
|
||||
)
|
||||
else:
|
||||
return ((n, n, nbrs[n]) for n, nbrs in G.adj.items() if n in nbrs)
|
||||
elif data is not False:
|
||||
if G.is_multigraph():
|
||||
if keys is True:
|
||||
return (
|
||||
(n, n, k, d.get(data, default))
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for k, d in nbrs[n].items()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(n, n, d.get(data, default))
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for d in nbrs[n].values()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(n, n, nbrs[n].get(data, default))
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
)
|
||||
else:
|
||||
if G.is_multigraph():
|
||||
if keys is True:
|
||||
return (
|
||||
(n, n, k) for n, nbrs in G.adj.items() if n in nbrs for k in nbrs[n]
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(n, n)
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for i in range(len(nbrs[n])) # for easy edge removal (#4068)
|
||||
)
|
||||
else:
|
||||
return ((n, n) for n, nbrs in G.adj.items() if n in nbrs)
|
||||
|
||||
|
||||
@hybrid("cpp_density")
|
||||
def density(G):
|
||||
r"""Returns the density of a graph.
|
||||
|
||||
The density for undirected graphs is
|
||||
|
||||
.. math::
|
||||
|
||||
d = \frac{2m}{n(n-1)},
|
||||
|
||||
and for directed graphs is
|
||||
|
||||
.. math::
|
||||
|
||||
d = \frac{m}{n(n-1)},
|
||||
|
||||
where `n` is the number of nodes and `m` is the number of edges in `G`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The density is 0 for a graph without edges and 1 for a complete graph.
|
||||
The density of multigraphs can be higher than 1.
|
||||
|
||||
Self loops are counted in the total number of edges so graphs with self
|
||||
loops can have density higher than 1.
|
||||
"""
|
||||
n = G.number_of_nodes()
|
||||
m = G.number_of_edges()
|
||||
if m == 0 or n <= 1:
|
||||
return 0
|
||||
d = m / (n * (n - 1))
|
||||
if not G.is_directed():
|
||||
d *= 2
|
||||
return d
|
||||
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
print(
|
||||
os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "../cpp_easygraph")
|
||||
)
|
||||
)
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
import easygraph.classes as cls # Spend 4.9s on importing this damn big lib.
|
||||
|
||||
|
||||
def test_iter():
|
||||
g = eg.Graph()
|
||||
# repeated endings test
|
||||
g.add_edge(None, None) # 1
|
||||
g.add_edge(True, False)
|
||||
|
||||
g.add_edge(0b1000, 100)
|
||||
print(g.edges)
|
||||
|
||||
|
||||
test_iter()
|
||||
@@ -0,0 +1,145 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
pd = pytest.importorskip("pandas")
|
||||
sp = pytest.importorskip("scipy")
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.misc import *
|
||||
|
||||
|
||||
class TestConvertNumpyArray:
|
||||
def setup_method(self):
|
||||
self.G1 = eg.complete_graph(5)
|
||||
|
||||
def assert_equal(self, G1, G2):
|
||||
assert nodes_equal(G1.nodes, G2.nodes)
|
||||
assert edges_equal(G1.edges, G2.edges, need_data=False)
|
||||
|
||||
def identity_conversion(self, G, A, create_using):
|
||||
assert A.sum() > 0
|
||||
GG = eg.from_numpy_array(A, create_using=create_using)
|
||||
self.assert_equal(G, GG)
|
||||
GW = eg.to_easygraph_graph(A, create_using=create_using)
|
||||
self.assert_equal(G, GW)
|
||||
|
||||
def test_identity_graph_array(self):
|
||||
A = eg.to_numpy_array(self.G1)
|
||||
self.identity_conversion(self.G1, A, eg.Graph())
|
||||
|
||||
|
||||
class TestConvertPandas:
|
||||
def setup_method(self):
|
||||
self.rng = np.random.RandomState(seed=5)
|
||||
ints = self.rng.randint(1, 11, size=(3, 2))
|
||||
a = ["A", "B", "C"]
|
||||
b = ["D", "A", "E"]
|
||||
df = pd.DataFrame(ints, columns=["weight", "cost"])
|
||||
df[0] = a
|
||||
df["b"] = b
|
||||
self.df = df
|
||||
|
||||
mdf = pd.DataFrame([[4, 16, "A", "D"]], columns=["weight", "cost", 0, "b"])
|
||||
self.mdf = pd.concat([df, mdf])
|
||||
|
||||
def assert_equal(self, G1, G2):
|
||||
assert nodes_equal(G1.nodes, G2.nodes)
|
||||
assert edges_equal(G1.edges, G2.edges, need_data=False)
|
||||
|
||||
def test_from_edgelist_multi_attr(self):
|
||||
Gtrue = eg.Graph(
|
||||
[
|
||||
("E", "C", {"cost": 9, "weight": 10}),
|
||||
("B", "A", {"cost": 1, "weight": 7}),
|
||||
("A", "D", {"cost": 7, "weight": 4}),
|
||||
]
|
||||
)
|
||||
G = eg.from_pandas_edgelist(self.df, 0, "b", ["weight", "cost"])
|
||||
self.assert_equal(G, Gtrue)
|
||||
|
||||
def test_from_adjacency(self):
|
||||
Gtrue = eg.DiGraph([("A", "B"), ("B", "C")])
|
||||
data = {
|
||||
"A": {"A": 0, "B": 0, "C": 0},
|
||||
"B": {"A": 1, "B": 0, "C": 0},
|
||||
"C": {"A": 0, "B": 1, "C": 0},
|
||||
}
|
||||
dftrue = pd.DataFrame(data, dtype=np.intp)
|
||||
df = dftrue[["A", "C", "B"]]
|
||||
G = eg.from_pandas_adjacency(df, create_using=eg.DiGraph())
|
||||
self.assert_equal(G, Gtrue)
|
||||
|
||||
|
||||
class TestConvertScipy:
|
||||
def setup_method(self):
|
||||
self.G1 = eg.complete_graph(3)
|
||||
|
||||
def assert_equal(self, G1, G2):
|
||||
assert nodes_equal(G1.nodes, G2.nodes)
|
||||
assert edges_equal(G1.edges, G2.edges, need_data=False)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 8), reason="requires python3.8 or higher"
|
||||
)
|
||||
def test_from_scipy(self):
|
||||
data = sp.sparse.csr_matrix([[0, 1, 1], [1, 0, 1], [1, 1, 0]])
|
||||
G = eg.from_scipy_sparse_matrix(data)
|
||||
self.assert_equal(self.G1, G)
|
||||
|
||||
|
||||
def test_from_edgelist():
|
||||
edgelist = [(0, 1), (1, 2)]
|
||||
G = eg.from_edgelist(edgelist)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)]
|
||||
|
||||
|
||||
def test_from_dict_of_lists():
|
||||
d = {0: [1], 1: [2]}
|
||||
G = eg.to_easygraph_graph(d)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)]
|
||||
|
||||
|
||||
def test_from_dict_of_dicts():
|
||||
d = {0: {1: {}}, 1: {2: {}}}
|
||||
G = eg.to_easygraph_graph(d)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)]
|
||||
|
||||
|
||||
def test_from_numpy_array():
|
||||
G = eg.complete_graph(3)
|
||||
A = eg.to_numpy_array(G)
|
||||
G2 = eg.from_numpy_array(A)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == sorted(
|
||||
(u, v) for u, v, _ in G2.edges
|
||||
)
|
||||
|
||||
|
||||
def test_from_pandas_edgelist():
|
||||
df = pd.DataFrame({"source": [0, 1], "target": [1, 2], "weight": [0.5, 0.7]})
|
||||
G = eg.from_pandas_edgelist(df, source="source", target="target", edge_attr=True)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)]
|
||||
|
||||
|
||||
def test_from_pandas_adjacency():
|
||||
df = pd.DataFrame([[0, 1], [1, 0]], columns=["A", "B"], index=["A", "B"])
|
||||
G = eg.from_pandas_adjacency(df)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [("A", "B")]
|
||||
|
||||
|
||||
def test_from_scipy_sparse_matrix():
|
||||
mat = sp.sparse.csr_matrix([[0, 1, 0], [1, 0, 1], [0, 1, 0]])
|
||||
G = eg.from_scipy_sparse_matrix(mat)
|
||||
expected_edges = [(0, 1), (1, 2)]
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == expected_edges
|
||||
|
||||
|
||||
def test_invalid_dict_type():
|
||||
class NotGraph:
|
||||
pass
|
||||
|
||||
with pytest.raises(eg.EasyGraphError):
|
||||
eg.to_easygraph_graph(NotGraph())
|
||||
@@ -0,0 +1,97 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from easygraph import DiGraph
|
||||
|
||||
|
||||
class TestDiGraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = DiGraph()
|
||||
|
||||
def test_add_node_and_exists(self):
|
||||
self.G.add_node("A")
|
||||
self.assertTrue(self.G.has_node("A"))
|
||||
self.assertIn("A", self.G.nodes)
|
||||
|
||||
def test_add_nodes_with_attrs(self):
|
||||
self.G.add_nodes(["B", "C"], nodes_attr=[{"age": 30}, {"age": 40}])
|
||||
self.assertEqual(self.G.nodes["B"]["age"], 30)
|
||||
self.assertEqual(self.G.nodes["C"]["age"], 40)
|
||||
|
||||
def test_add_edge_and_attrs(self):
|
||||
self.G.add_edge("A", "B", weight=5)
|
||||
self.assertTrue(self.G.has_edge("A", "B"))
|
||||
self.assertEqual(self.G.adj["A"]["B"]["weight"], 5)
|
||||
|
||||
def test_add_edges_with_attrs(self):
|
||||
self.G.add_edges([("B", "C"), ("C", "D")], edges_attr=[{"w": 1}, {"w": 2}])
|
||||
self.assertEqual(self.G.adj["B"]["C"]["w"], 1)
|
||||
self.assertEqual(self.G.adj["C"]["D"]["w"], 2)
|
||||
|
||||
def test_remove_node_and_edges(self):
|
||||
self.G.add_edges([("X", "Y"), ("Y", "Z")])
|
||||
self.G.remove_node("Y")
|
||||
self.assertFalse("Y" in self.G.nodes)
|
||||
self.assertFalse(self.G.has_edge("Y", "Z"))
|
||||
|
||||
def test_remove_edge(self):
|
||||
self.G.add_edge("M", "N")
|
||||
self.G.remove_edge("M", "N")
|
||||
self.assertFalse(self.G.has_edge("M", "N"))
|
||||
|
||||
def test_degrees(self):
|
||||
self.G.add_edges(
|
||||
[("A", "B"), ("C", "B")], edges_attr=[{"weight": 3}, {"weight": 2}]
|
||||
)
|
||||
|
||||
in_degrees = self.G.in_degree(weight="weight")
|
||||
out_degrees = self.G.out_degree(weight="weight")
|
||||
degrees = self.G.degree(weight="weight")
|
||||
|
||||
self.assertEqual(in_degrees["B"], 5)
|
||||
self.assertEqual(out_degrees["A"], 3)
|
||||
self.assertEqual(degrees["B"], 5)
|
||||
|
||||
def test_neighbors_and_preds(self):
|
||||
self.G.add_edges([("P", "Q"), ("R", "P")])
|
||||
self.assertIn("Q", list(self.G.neighbors("P")))
|
||||
self.assertIn("R", list(self.G.predecessors("P")))
|
||||
all_n = list(self.G.all_neighbors("P"))
|
||||
self.assertIn("Q", all_n)
|
||||
self.assertIn("R", all_n)
|
||||
|
||||
def test_size_and_num_edges_nodes(self):
|
||||
self.G.add_edges([("X", "Y"), ("Y", "Z")])
|
||||
self.assertEqual(self.G.size(), 2)
|
||||
self.assertEqual(self.G.number_of_edges(), 2)
|
||||
self.assertEqual(self.G.number_of_nodes(), 3)
|
||||
|
||||
def test_subgraph_and_ego(self):
|
||||
self.G.add_edges([("A", "B"), ("B", "C"), ("C", "D")])
|
||||
sub = self.G.nodes_subgraph(["A", "B", "C"])
|
||||
self.assertTrue(sub.has_edge("A", "B"))
|
||||
self.assertFalse(sub.has_edge("C", "D"))
|
||||
ego = self.G.ego_subgraph("B")
|
||||
self.assertIn("A", ego.nodes or [])
|
||||
self.assertIn("C", ego.nodes or [])
|
||||
|
||||
def test_to_index_node_graph(self):
|
||||
self.G.add_edges([("foo", "bar"), ("bar", "baz")])
|
||||
G2, node2idx, idx2node = self.G.to_index_node_graph()
|
||||
self.assertEqual(len(G2.nodes), 3)
|
||||
self.assertEqual(node2idx["foo"], 0)
|
||||
self.assertEqual(idx2node[0], "foo")
|
||||
|
||||
def test_copy(self):
|
||||
self.G.add_edge("copyA", "copyB", weight=42)
|
||||
G_copy = self.G.copy()
|
||||
self.assertEqual(G_copy.adj["copyA"]["copyB"]["weight"], 42)
|
||||
|
||||
def test_file_add_edges(self):
|
||||
fname = "temp_edges.txt"
|
||||
with open(fname, "w") as f:
|
||||
f.write("1 2 3.5\n2 3 4.5\n")
|
||||
self.G.add_edges_from_file(fname, weighted=True)
|
||||
os.remove(fname)
|
||||
self.assertEqual(self.G.adj["1"]["2"]["weight"], 3.5)
|
||||
self.assertEqual(self.G.adj["2"]["3"]["weight"], 4.5)
|
||||
@@ -0,0 +1,112 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
# sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),'..', '..')))
|
||||
import easygraph as eg # Spend 4.9s on importing this damn big lib.
|
||||
|
||||
|
||||
"""
|
||||
def test_iter():
|
||||
g = eg.Graph()
|
||||
# tests of corner cases
|
||||
|
||||
g.add_edge(0, 0)
|
||||
g.add_edge(True, False)
|
||||
g.add_edge(False, 1)
|
||||
g.add_edge(0b1000, 0x00a, edge_attr={"age": 19, "gender": "Male"})
|
||||
# g.add_edge(None, None) # this shall result in an AssertionError
|
||||
# g.add_edge(None, 1) # this shall result in an AssertionError
|
||||
# g.add_edge(1, None) # this shall result in an AssertionError
|
||||
|
||||
# g.add_edges(None) # Triggers a TypeError saying that len() is not applicable to None
|
||||
g.add_edges([(True, False), ("Beijing National", "Day School")], [{}, {"Rating": 100}])
|
||||
g.add_node("FuDan Univ", node_attr={"faculty": 10000}) # 1.
|
||||
g.add_edge("Beijing National", "FuDan Univ")
|
||||
# g.add_node([]) # this shall result in an unhashable error
|
||||
g.add_node('Jack', node_attr={
|
||||
'age': 10,
|
||||
'gender': 'M'
|
||||
})
|
||||
# g.remove_node("Beijing National")
|
||||
g.remove_edges([('Day School', 'Beijing National')])
|
||||
# g.add_edges_from()
|
||||
|
||||
print(g.add_extra_selfloop())
|
||||
|
||||
|
||||
g.nbr_v()
|
||||
g.nbunch_iter()
|
||||
g.from_hypergraph_hypergcn()
|
||||
# print(g._adj[8].get(10))
|
||||
print(g.edges)
|
||||
print(g.nodes)
|
||||
|
||||
|
||||
test_iter()
|
||||
"""
|
||||
|
||||
from easygraph.datasets import get_graph_karateclub
|
||||
|
||||
|
||||
G = get_graph_karateclub()
|
||||
# Calculate five shs(Structural Hole Spanners) in G
|
||||
shs = eg.common_greedy(G, 5)
|
||||
# Draw the Graph, and the shs is marked by a red star
|
||||
eg.draw_SHS_center(G, shs)
|
||||
# Draw CDF curves of "Number of Followers" of SH spanners and ordinary users in G.
|
||||
eg.plot_Followers(G, shs)
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
G = eg.Graph()
|
||||
G.add_edge(1, 2) # Add a single edge
|
||||
print(G.edges)
|
||||
|
||||
G.add_edges([(2, 3), (1, 3), (3, 4), (4, 5), ((1, 2), (3, 4))]) # Add edges
|
||||
print(G.edges)
|
||||
|
||||
|
||||
G.add_node("hello world")
|
||||
G.add_node("Jack", node_attr={"age": 10, "gender": "M"})
|
||||
print(G.nodes)
|
||||
|
||||
# G.remove_nodes(['hello world','Tom','Lily','a','b'])#remove edges
|
||||
G.remove_nodes(["hello world"])
|
||||
print(G.nodes)
|
||||
|
||||
G.remove_edge(4, 5)
|
||||
print(G.edges)
|
||||
|
||||
print(len(G)) # __len__(self)
|
||||
for x in G: # __iter__(self)
|
||||
print(x)
|
||||
print(G[1]) # return list(self._adj[node].keys()) __contains__ __getitem__
|
||||
|
||||
for neighbor in G.neighbors(node=2):
|
||||
print(neighbor)
|
||||
|
||||
G.add_edges(
|
||||
[(1, 2), (2, 3), (1, 3), (3, 4), (4, 5)],
|
||||
edges_attr=[
|
||||
{"weight": 20},
|
||||
{"weight": 10},
|
||||
{"weight": 15},
|
||||
{"weight": 8},
|
||||
{"weight": 12},
|
||||
],
|
||||
) # add weighted edges
|
||||
G.add_node(6)
|
||||
print(G.edges)
|
||||
|
||||
print(G.degree())
|
||||
print(G.degree(weight="weight"))
|
||||
|
||||
G_index_graph, index_of_node, node_of_index = G.to_index_node_graph()
|
||||
print(G_index_graph.adj)
|
||||
|
||||
G1 = G.copy()
|
||||
print(G1.adj)
|
||||
|
||||
print(eg.effective_size(G))
|
||||
@@ -0,0 +1,122 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestEasyGraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.Graph()
|
||||
|
||||
def test_add_single_node(self):
|
||||
self.G.add_node(1)
|
||||
self.assertIn(1, self.G.nodes)
|
||||
|
||||
def test_add_multiple_nodes(self):
|
||||
self.G.add_nodes([2, 3, 4])
|
||||
for node in [2, 3, 4]:
|
||||
self.assertIn(node, self.G.nodes)
|
||||
|
||||
def test_add_node_with_attributes(self):
|
||||
self.G.add_node("node", color="red")
|
||||
self.assertEqual(self.G.nodes["node"]["color"], "red")
|
||||
|
||||
def test_add_single_edge(self):
|
||||
self.G.add_edge(1, 2)
|
||||
self.assertTrue(self.G.has_edge(1, 2))
|
||||
self.assertTrue(self.G.has_edge(2, 1))
|
||||
|
||||
def test_add_edge_with_weight(self):
|
||||
self.G.add_edge("a", "b", weight=10)
|
||||
self.assertEqual(self.G["a"]["b"]["weight"], 10)
|
||||
|
||||
def test_add_edges(self):
|
||||
self.G.add_edges([(1, 2), (2, 3)], edges_attr=[{"weight": 5}, {"weight": 6}])
|
||||
self.assertEqual(self.G[1][2]["weight"], 5)
|
||||
self.assertEqual(self.G[2][3]["weight"], 6)
|
||||
|
||||
def test_remove_node(self):
|
||||
self.G.add_node(10)
|
||||
self.G.remove_node(10)
|
||||
self.assertNotIn(10, self.G.nodes)
|
||||
|
||||
def test_remove_edge(self):
|
||||
self.G.add_edge(1, 2)
|
||||
self.G.remove_edge(1, 2)
|
||||
self.assertFalse(self.G.has_edge(1, 2))
|
||||
|
||||
def test_neighbors(self):
|
||||
self.G.add_edges([(1, 2), (1, 3)])
|
||||
neighbors = list(self.G.neighbors(1))
|
||||
self.assertIn(2, neighbors)
|
||||
self.assertIn(3, neighbors)
|
||||
|
||||
def test_subgraph(self):
|
||||
self.G.add_edges([(1, 2), (2, 3), (3, 4)])
|
||||
subG = self.G.nodes_subgraph([2, 3])
|
||||
self.assertIn(2, subG.nodes)
|
||||
self.assertIn(3, subG.nodes)
|
||||
self.assertTrue(subG.has_edge(2, 3))
|
||||
self.assertFalse(subG.has_edge(3, 4))
|
||||
|
||||
def test_ego_subgraph(self):
|
||||
self.G.add_edges([(1, 2), (2, 3), (2, 4)])
|
||||
ego = self.G.ego_subgraph(2)
|
||||
self.assertIn(2, ego.nodes)
|
||||
self.assertIn(1, ego.nodes)
|
||||
self.assertIn(3, ego.nodes)
|
||||
self.assertIn(4, ego.nodes)
|
||||
|
||||
def test_to_index_node_graph(self):
|
||||
self.G.add_edges([("a", "b"), ("b", "c")])
|
||||
G_index, index_of_node, node_of_index = self.G.to_index_node_graph()
|
||||
self.assertEqual(len(G_index.nodes), 3)
|
||||
self.assertTrue(all(isinstance(k, int) for k in G_index.nodes))
|
||||
|
||||
def test_directed_conversion(self):
|
||||
self.G.add_edge(1, 2)
|
||||
H = self.G.to_directed()
|
||||
self.assertTrue(H.is_directed())
|
||||
self.assertTrue(H.has_edge(1, 2))
|
||||
self.assertTrue(H.has_edge(2, 1))
|
||||
|
||||
def test_clone_graph(self):
|
||||
self.G.add_edges([(1, 2), (2, 3)])
|
||||
G_clone = self.G.copy()
|
||||
self.assertTrue(G_clone.has_edge(1, 2))
|
||||
self.assertTrue(G_clone.has_edge(2, 3))
|
||||
|
||||
def test_degree(self):
|
||||
self.G.add_edge(1, 2, weight=5)
|
||||
deg = self.G.degree()
|
||||
self.assertEqual(deg[1], 5)
|
||||
self.assertEqual(deg[2], 5)
|
||||
|
||||
def test_size(self):
|
||||
self.G.add_edges([(1, 2), (2, 3)])
|
||||
self.assertEqual(self.G.size(), 2)
|
||||
|
||||
def test_edge_weight_default(self):
|
||||
self.G.add_edge(4, 5)
|
||||
self.assertEqual(self.G[4][5].get("weight", 1), 1)
|
||||
|
||||
def test_node_index_mappings(self):
|
||||
self.G.add_nodes([10, 20, 30])
|
||||
index2node = self.G.index2node
|
||||
node_index = self.G.node_index
|
||||
for i, node in index2node.items():
|
||||
self.assertEqual(node_index[node], i)
|
||||
|
||||
def test_graph_order(self):
|
||||
self.G.add_nodes([1, 2, 3])
|
||||
self.assertEqual(self.G.order(), 3)
|
||||
|
||||
def test_graph_size_with_weight(self):
|
||||
self.G.add_edges([(1, 2), (2, 3)], edges_attr=[{"weight": 4}, {"weight": 6}])
|
||||
self.assertEqual(self.G.size(weight="weight"), 10.0)
|
||||
|
||||
def test_clear_cache(self):
|
||||
self.G.add_edge(1, 2)
|
||||
_ = self.G.edges
|
||||
self.assertIn("edge", self.G.cache)
|
||||
self.G._clear_cache()
|
||||
self.assertEqual(len(self.G.cache), 0)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
def setUp(self):
|
||||
edges = [(1, 2), (2, 3), ("String", "Bool"), (2, 1), ((1, 2), (3, 4))]
|
||||
self.g = eg.MultiDiGraph(edges)
|
||||
|
||||
def test_add_edge(self):
|
||||
self.g.add_edge("from_Beijing", "to_California", key=3, attr=None)
|
||||
print(self.g.edges)
|
||||
|
||||
def test_remove_edge(self):
|
||||
self.g.add_edge("from_Beijing", "to_California", key=3, attr=None)
|
||||
self.g.remove_edge("from_Beijing", "to_California")
|
||||
print(self.g.edges)
|
||||
|
||||
def test_degree(self):
|
||||
print(self.g.degree)
|
||||
print(self.g.in_degree)
|
||||
print(self.g.out_degree)
|
||||
|
||||
def test_reverse(self):
|
||||
# error with _succ
|
||||
print(self.g.reverse(copy=True).edges)
|
||||
# print(self.g.reverse(copy=False).edges)
|
||||
|
||||
def test_attributes(self):
|
||||
print(self.g.edges)
|
||||
print(self.g.in_edges)
|
||||
|
||||
|
||||
class TestMultiDiGraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.MultiDiGraph()
|
||||
|
||||
def test_add_edge_without_key(self):
|
||||
key1 = self.G.add_edge("A", "B", weight=1)
|
||||
key2 = self.G.add_edge("A", "B", weight=2)
|
||||
self.assertNotEqual(key1, key2)
|
||||
self.assertEqual(len(self.G._adj["A"]["B"]), 2)
|
||||
|
||||
def test_add_edge_with_key(self):
|
||||
key = self.G.add_edge("A", "B", key="mykey", weight=3)
|
||||
self.assertEqual(key, "mykey")
|
||||
self.assertEqual(self.G._adj["A"]["B"]["mykey"]["weight"], 3)
|
||||
|
||||
def test_edge_attributes_update(self):
|
||||
self.G.add_edge("X", "Y", key=1, color="red")
|
||||
self.G.add_edge("X", "Y", key=1, shape="circle")
|
||||
self.assertEqual(self.G._adj["X"]["Y"][1]["color"], "red")
|
||||
self.assertEqual(self.G._adj["X"]["Y"][1]["shape"], "circle")
|
||||
|
||||
def test_remove_edge_by_key(self):
|
||||
self.G.add_edge("A", "B", key="k1")
|
||||
self.G.add_edge("A", "B", key="k2")
|
||||
self.G.remove_edge("A", "B", key="k1")
|
||||
self.assertIn("k2", self.G._adj["A"]["B"])
|
||||
self.assertNotIn("k1", self.G._adj["A"]["B"])
|
||||
|
||||
def test_remove_edge_without_key(self):
|
||||
self.G.add_edge("A", "B", key="auto1")
|
||||
self.G.add_edge("A", "B", key="auto2")
|
||||
self.G.remove_edge("A", "B")
|
||||
# Only one of the keys should remain
|
||||
self.assertEqual(len(self.G._adj["A"]["B"]), 1)
|
||||
|
||||
def test_remove_nonexistent_edge_raises(self):
|
||||
with self.assertRaises(eg.EasyGraphError):
|
||||
self.G.remove_edge("X", "Y", key="doesnotexist")
|
||||
|
||||
def test_edges_property(self):
|
||||
self.G.add_edge("U", "V", key="k", weight=5)
|
||||
edges = self.G.edges
|
||||
self.assertIn(("U", "V", "k", {"weight": 5}), edges)
|
||||
|
||||
def test_in_out_degree(self):
|
||||
self.G.add_edge("A", "B", weight=3)
|
||||
self.G.add_edge("C", "B", weight=2)
|
||||
|
||||
in_deg = {}
|
||||
for n in self.G._node:
|
||||
preds = self.G._pred[n]
|
||||
in_deg[n] = sum(
|
||||
d.get("weight", 1)
|
||||
for key_dict in preds.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
|
||||
self.assertEqual(in_deg["B"], 5)
|
||||
|
||||
def test_to_undirected(self):
|
||||
self.G.add_edge("A", "B", key="k", weight=10)
|
||||
UG = self.G.to_undirected()
|
||||
self.assertTrue(UG.has_edge("A", "B"))
|
||||
self.assertEqual(UG["A"]["B"]["k"]["weight"], 10)
|
||||
|
||||
def test_reverse_graph(self):
|
||||
self.G.add_edge("A", "B", key="k", data=99)
|
||||
RG = self.G.reverse()
|
||||
self.assertTrue(RG.has_edge("B", "A"))
|
||||
self.assertEqual(RG["B"]["A"]["k"]["data"], 99)
|
||||
|
||||
def test_is_multigraph_and_directed(self):
|
||||
self.assertTrue(self.G.is_multigraph())
|
||||
self.assertTrue(self.G.is_directed())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
# test()
|
||||
@@ -0,0 +1,149 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class TestMultiGraph:
|
||||
def setup_method(self):
|
||||
self.Graph = eg.MultiGraph
|
||||
# build K3
|
||||
ed1, ed2, ed3 = ({0: {}}, {0: {}}, {0: {}})
|
||||
self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed1, 2: ed3}, 2: {0: ed2, 1: ed3}}
|
||||
self.k3edges = [(0, 1), (0, 2), (1, 2)]
|
||||
self.k3nodes = [0, 1, 2]
|
||||
self.K3 = self.Graph()
|
||||
self.K3._adj = self.k3adj
|
||||
self.K3._node = {}
|
||||
self.K3._node[0] = {}
|
||||
self.K3._node[1] = {}
|
||||
self.K3._node[2] = {}
|
||||
|
||||
def test_data_input(self):
|
||||
G = self.Graph({1: [2], 2: [1]}, name="test")
|
||||
assert G.name == "test"
|
||||
expected = [(1, {2: {0: {}}}), (2, {1: {0: {}}})]
|
||||
assert sorted(G.adj.items()) == expected
|
||||
|
||||
def test_has_edge(self):
|
||||
G = self.K3
|
||||
assert G.has_edge(0, 1)
|
||||
assert not G.has_edge(0, -1)
|
||||
assert G.has_edge(0, 1, 0)
|
||||
assert not G.has_edge(0, 1, 1)
|
||||
|
||||
def test_get_edge_data(self):
|
||||
G = self.K3
|
||||
assert G.get_edge_data(0, 1) == {0: {}}
|
||||
assert G[0][1] == {0: {}}
|
||||
assert G[0][1][0] == {}
|
||||
assert G.get_edge_data(10, 20) is None
|
||||
assert G.get_edge_data(0, 1, 0) == {}
|
||||
|
||||
def test_data_multigraph_input(self):
|
||||
# standard case with edge keys and edge data
|
||||
edata0 = dict(w=200, s="foo")
|
||||
edata1 = dict(w=201, s="bar")
|
||||
keydict = {0: edata0, 1: edata1}
|
||||
dododod = {"a": {"b": keydict}}
|
||||
|
||||
multiple_edge = [("a", "b", 0, edata0), ("a", "b", 1, edata1)]
|
||||
single_edge = [("a", "b", 0, keydict)]
|
||||
|
||||
G = self.Graph(dododod, multigraph_input=None)
|
||||
assert list(G.edges) == multiple_edge
|
||||
G = self.Graph(dododod, multigraph_input=False)
|
||||
assert list(G.edges) == single_edge
|
||||
|
||||
def test_remove_node(self):
|
||||
G = self.K3
|
||||
G.remove_node(0)
|
||||
assert G.adj == {1: {2: {0: {}}}, 2: {1: {0: {}}}}
|
||||
with pytest.raises(eg.EasyGraphError):
|
||||
G.remove_node(-1)
|
||||
|
||||
|
||||
class TestMultiGraphExtended:
|
||||
def test_add_multiple_edges_and_keys(self):
|
||||
G = eg.MultiGraph()
|
||||
k0 = G.add_edge(1, 2)
|
||||
k1 = G.add_edge(1, 2)
|
||||
assert k0 == 0
|
||||
assert k1 == 1
|
||||
assert G.number_of_edges(1, 2) == 2
|
||||
|
||||
def test_add_edge_with_key_and_attributes(self):
|
||||
G = eg.MultiGraph()
|
||||
k = G.add_edge(1, 2, key="custom", weight=3, label="test")
|
||||
assert k == "custom"
|
||||
assert G.get_edge_data(1, 2, "custom") == {"weight": 3, "label": "test"}
|
||||
|
||||
def test_add_edges_from_various_formats(self):
|
||||
G = eg.MultiGraph()
|
||||
edges = [
|
||||
(1, 2), # 2-tuple
|
||||
(2, 3, {"weight": 7}), # 3-tuple with attr
|
||||
(3, 4, "k1", {"color": "red"}), # 4-tuple
|
||||
]
|
||||
keys = G.add_edges_from(edges, capacity=100)
|
||||
assert len(keys) == 3
|
||||
assert G.get_edge_data(3, 4, "k1")["color"] == "red"
|
||||
assert G.get_edge_data(2, 3, 0)["capacity"] == 100
|
||||
|
||||
def test_remove_edge_with_key(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(1, 2, key="a")
|
||||
G.add_edge(1, 2, key="b")
|
||||
G.remove_edge(1, 2, key="a")
|
||||
assert not G.has_edge(1, 2, key="a")
|
||||
assert G.has_edge(1, 2, key="b")
|
||||
|
||||
def test_remove_edge_arbitrary(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(1, 2)
|
||||
G.add_edge(1, 2)
|
||||
G.remove_edge(1, 2)
|
||||
assert G.number_of_edges(1, 2) == 1
|
||||
|
||||
def test_remove_edges_from_mixed(self):
|
||||
G = eg.MultiGraph()
|
||||
keys = G.add_edges_from([(1, 2), (1, 2), (2, 3)])
|
||||
G.remove_edges_from([(1, 2), (2, 3)])
|
||||
assert G.number_of_edges(1, 2) == 1
|
||||
assert G.number_of_edges(2, 3) == 0
|
||||
|
||||
def test_to_directed_graph(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(0, 1, weight=10)
|
||||
D = G.to_directed()
|
||||
assert D.is_directed()
|
||||
assert D.has_edge(0, 1)
|
||||
assert D.has_edge(1, 0)
|
||||
assert D.get_edge_data(0, 1, 0)["weight"] == 10
|
||||
|
||||
def test_copy_graph(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(1, 2, key="x", weight=9)
|
||||
H = G.copy()
|
||||
assert H.get_edge_data(1, 2, "x") == {"weight": 9}
|
||||
assert H is not G
|
||||
assert H.get_edge_data(1, 2, "x") is not G.get_edge_data(1, 2, "x")
|
||||
|
||||
def test_has_edge_variants(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(1, 2)
|
||||
G.add_edge(1, 2, key="z")
|
||||
assert G.has_edge(1, 2)
|
||||
assert G.has_edge(1, 2, key="z")
|
||||
assert not G.has_edge(2, 1, key="nonexistent")
|
||||
|
||||
def test_get_edge_data_defaults(self):
|
||||
G = eg.MultiGraph()
|
||||
assert G.get_edge_data(10, 20) is None
|
||||
assert G.get_edge_data(10, 20, key="any", default="missing") == "missing"
|
||||
|
||||
def test_edge_property_returns_all_edges(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(0, 1, key=5, label="important")
|
||||
G.add_edge(1, 0, key=3, label="also important")
|
||||
edges = list(G.edges)
|
||||
assert any((0, 1, 5, {"label": "important"}) == e for e in edges)
|
||||
assert any((0, 1, 3, {"label": "also important"}) == e for e in edges)
|
||||
@@ -0,0 +1,131 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph.classes import operation
|
||||
from easygraph.utils import edges_equal
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"graph_type", [eg.Graph, eg.DiGraph, eg.MultiGraph, eg.MultiDiGraph]
|
||||
)
|
||||
def test_selfloops(graph_type):
|
||||
G = eg.complete_graph(3, create_using=graph_type)
|
||||
G.add_edge(0, 0)
|
||||
assert edges_equal(eg.selfloop_edges(G), [(0, 0)])
|
||||
assert edges_equal(eg.selfloop_edges(G, data=True), [(0, 0, {})])
|
||||
assert eg.number_of_selfloops(G) == 1
|
||||
|
||||
|
||||
def test_set_edge_attributes_scalar():
|
||||
G = eg.path_graph(3)
|
||||
eg.set_edge_attributes(G, 5, "weight")
|
||||
for _, _, data in G.edges:
|
||||
assert data["weight"] == 5
|
||||
|
||||
|
||||
def test_set_edge_attributes_dict():
|
||||
G = eg.path_graph(3)
|
||||
attrs = {(0, 1): 3, (1, 2): 7}
|
||||
eg.set_edge_attributes(G, attrs, "weight")
|
||||
assert G[0][1]["weight"] == 3
|
||||
assert G[1][2]["weight"] == 7
|
||||
|
||||
|
||||
def test_set_edge_attributes_dict_of_dict():
|
||||
G = eg.path_graph(3)
|
||||
attrs = {(0, 1): {"a": 1}, (1, 2): {"b": 2}}
|
||||
eg.set_edge_attributes(G, attrs)
|
||||
assert G[0][1]["a"] == 1
|
||||
assert G[1][2]["b"] == 2
|
||||
|
||||
|
||||
def test_set_node_attributes_scalar():
|
||||
G = eg.path_graph(3)
|
||||
eg.set_node_attributes(G, 42, "level")
|
||||
for n in G.nodes:
|
||||
assert G.nodes[n]["level"] == 42
|
||||
|
||||
|
||||
def test_set_node_attributes_dict():
|
||||
G = eg.path_graph(3)
|
||||
eg.set_node_attributes(G, {0: "x", 1: "y"}, name="tag")
|
||||
assert G.nodes[0]["tag"] == "x"
|
||||
assert G.nodes[1]["tag"] == "y"
|
||||
|
||||
|
||||
def test_set_node_attributes_dict_of_dict():
|
||||
G = eg.path_graph(3)
|
||||
eg.set_node_attributes(G, {0: {"foo": 10}, 1: {"bar": 20}})
|
||||
assert G.nodes[0]["foo"] == 10
|
||||
assert G.nodes[1]["bar"] == 20
|
||||
|
||||
|
||||
def test_add_path_structure_and_attrs():
|
||||
G = eg.Graph()
|
||||
eg.add_path(G, [10, 11, 12], weight=9)
|
||||
actual_edges = {(u, v) for u, v, _ in G.edges}
|
||||
assert actual_edges == {(10, 11), (11, 12)}
|
||||
assert G[10][11]["weight"] == 9
|
||||
assert G[11][12]["weight"] == 9
|
||||
|
||||
|
||||
def test_topological_sort_linear():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
assert list(operation.topological_sort(G)) == [1, 2, 3]
|
||||
|
||||
|
||||
def test_topological_sort_cycle():
|
||||
G = eg.DiGraph([(0, 1), (1, 2), (2, 0)])
|
||||
with pytest.raises(AssertionError, match="contains a cycle"):
|
||||
list(operation.topological_sort(G))
|
||||
|
||||
|
||||
def test_selfloop_edges_variants():
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(0, 0, key="x", label="loop")
|
||||
G.add_edge(1, 1, key="y", label="loop2")
|
||||
basic = list(eg.selfloop_edges(G))
|
||||
with_data = list(eg.selfloop_edges(G, data=True))
|
||||
with_keys = list(eg.selfloop_edges(G, keys=True))
|
||||
full = list(eg.selfloop_edges(G, keys=True, data="label"))
|
||||
assert (0, 0) in basic and (1, 1) in basic
|
||||
assert all(len(t) == 3 for t in with_data)
|
||||
assert all(len(t) == 3 for t in with_keys)
|
||||
assert "x" in [k for _, _, k, _ in full]
|
||||
|
||||
|
||||
def test_number_of_selfloops():
|
||||
G = eg.MultiGraph()
|
||||
G.add_edges_from([(0, 0), (1, 1), (1, 2)])
|
||||
assert eg.number_of_selfloops(G) == 2
|
||||
|
||||
|
||||
def test_density_undirected():
|
||||
G = eg.complete_graph(5)
|
||||
d = eg.density(G)
|
||||
assert pytest.approx(d, 0.01) == 1.0
|
||||
|
||||
|
||||
def test_density_directed():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 2)])
|
||||
d = eg.density(G)
|
||||
assert pytest.approx(d, 0.01) == 2 / (3 * (3 - 1)) # 2/6
|
||||
|
||||
|
||||
def test_topological_generations_linear():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (2, 3), (3, 4)])
|
||||
generations = list(operation.topological_generations(G))
|
||||
assert generations == [[1], [2], [3], [4]]
|
||||
|
||||
|
||||
def test_topological_generations_branching():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (1, 3), (2, 4), (3, 4)])
|
||||
generations = list(operation.topological_generations(G))
|
||||
# Valid topological generations: [1], [2, 3], [4]
|
||||
assert generations[0] == [1]
|
||||
assert set(generations[1]) == {2, 3}
|
||||
assert generations[2] == [4]
|
||||
@@ -0,0 +1,591 @@
|
||||
import warnings
|
||||
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import dgl
|
||||
import networkx as nx
|
||||
import torch_geometric
|
||||
|
||||
from easygraph import DiGraph
|
||||
from easygraph import Graph
|
||||
|
||||
__all__ = [
|
||||
"from_dict_of_dicts",
|
||||
"to_easygraph_graph",
|
||||
"from_edgelist",
|
||||
"from_dict_of_lists",
|
||||
"from_networkx",
|
||||
"from_dgl",
|
||||
"from_pyg",
|
||||
"to_networkx",
|
||||
"to_dgl",
|
||||
"to_pyg",
|
||||
"dict_to_hypergraph",
|
||||
]
|
||||
|
||||
|
||||
def to_easygraph_graph(data, create_using=None, multigraph_input=False):
|
||||
"""Make a EasyGraph graph from a known data structure.
|
||||
|
||||
The preferred way to call this is automatically
|
||||
from the class constructor
|
||||
|
||||
>>> d = {0: {1: {"weight": 1}}} # dict-of-dicts single edge (0,1)
|
||||
>>> G = eg.Graph(d)
|
||||
|
||||
instead of the equivalent
|
||||
|
||||
>>> G = eg.from_dict_of_dicts(d)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : object to be converted
|
||||
|
||||
Current known types are:
|
||||
any EasyGraph graph
|
||||
dict-of-dicts
|
||||
dict-of-lists
|
||||
container (e.g. set, list, tuple) of edges
|
||||
iterator (e.g. itertools.chain) that produces edges
|
||||
generator of edges
|
||||
Pandas DataFrame (row per edge)
|
||||
numpy matrix
|
||||
numpy ndarray
|
||||
scipy sparse matrix
|
||||
pygraphviz agraph
|
||||
|
||||
create_using : EasyGraph graph constructor, optional (default=eg.Graph)
|
||||
Graph type to create. If graph instance, then cleared before populated.
|
||||
|
||||
multigraph_input : bool (default False)
|
||||
If True and data is a dict_of_dicts,
|
||||
try to create a multigraph assuming dict_of_dict_of_lists.
|
||||
If data and create_using are both multigraphs then create
|
||||
a multigraph from a multigraph.
|
||||
|
||||
"""
|
||||
|
||||
# EasyGraph graph type
|
||||
if hasattr(data, "adj"):
|
||||
try:
|
||||
result = from_dict_of_dicts(
|
||||
data.adj,
|
||||
create_using=create_using,
|
||||
multigraph_input=data.is_multigraph(),
|
||||
)
|
||||
# data.graph should be dict-like
|
||||
result.graph.update(data.graph)
|
||||
# data.nodes should be dict-like
|
||||
# result.add_node_from(data.nodes.items()) possible but
|
||||
# for custom node_attr_dict_factory which may be hashable
|
||||
# will be unexpected behavior
|
||||
for n, dd in data.nodes.items():
|
||||
result._node[n].update(dd)
|
||||
return result
|
||||
except Exception as err:
|
||||
raise eg.EasyGraphError("Input is not a correct EasyGraph graph.") from err
|
||||
|
||||
# pygraphviz agraph
|
||||
if hasattr(data, "is_strict"):
|
||||
try:
|
||||
return eg.from_pyGraphviz_agraph(data, create_using=create_using)
|
||||
except Exception as err:
|
||||
raise eg.EasyGraphError("Input is not a correct pygraphviz graph.") from err
|
||||
|
||||
# dict of dicts/lists
|
||||
if isinstance(data, dict):
|
||||
try:
|
||||
return from_dict_of_dicts(
|
||||
data, create_using=create_using, multigraph_input=multigraph_input
|
||||
)
|
||||
except Exception as err:
|
||||
if multigraph_input is True:
|
||||
raise eg.EasyGraphError(
|
||||
f"converting multigraph_input raised:\n{type(err)}: {err}"
|
||||
)
|
||||
try:
|
||||
return from_dict_of_lists(data, create_using=create_using)
|
||||
except Exception as err:
|
||||
raise TypeError("Input is not known type.") from err
|
||||
|
||||
# Pandas DataFrame
|
||||
try:
|
||||
import pandas as pd
|
||||
|
||||
if isinstance(data, pd.DataFrame):
|
||||
if data.shape[0] == data.shape[1]:
|
||||
try:
|
||||
return eg.from_pandas_adjacency(data, create_using=create_using)
|
||||
except Exception as err:
|
||||
msg = "Input is not a correct Pandas DataFrame adjacency matrix."
|
||||
raise eg.EasyGraphError(msg) from err
|
||||
else:
|
||||
try:
|
||||
return eg.from_pandas_edgelist(
|
||||
data, edge_attr=True, create_using=create_using
|
||||
)
|
||||
except Exception as err:
|
||||
msg = "Input is not a correct Pandas DataFrame adjacency edge-list."
|
||||
raise eg.EasyGraphError(msg) from err
|
||||
except ImportError:
|
||||
warnings.warn("pandas not found, skipping conversion test.", ImportWarning)
|
||||
|
||||
# numpy matrix or ndarray
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
if isinstance(data, np.ndarray):
|
||||
try:
|
||||
return eg.from_numpy_array(data, create_using=create_using)
|
||||
except Exception as err:
|
||||
raise eg.EasyGraphError(
|
||||
"Input is not a correct numpy matrix or array."
|
||||
) from err
|
||||
except ImportError:
|
||||
warnings.warn("numpy not found, skipping conversion test.", ImportWarning)
|
||||
|
||||
# scipy sparse matrix - any format
|
||||
try:
|
||||
if hasattr(data, "format"):
|
||||
try:
|
||||
return eg.from_scipy_sparse_matrix(data, create_using=create_using)
|
||||
except Exception as err:
|
||||
raise eg.EasyGraphError(
|
||||
"Input is not a correct scipy sparse matrix type."
|
||||
) from err
|
||||
except ImportError:
|
||||
warnings.warn("scipy not found, skipping conversion test.", ImportWarning)
|
||||
|
||||
# Note: most general check - should remain last in order of execution
|
||||
# Includes containers (e.g. list, set, dict, etc.), generators, and
|
||||
# iterators (e.g. itertools.chain) of edges
|
||||
|
||||
if isinstance(data, (Collection, Generator, Iterator)):
|
||||
try:
|
||||
return from_edgelist(data, create_using=create_using)
|
||||
except Exception as err:
|
||||
raise eg.EasyGraphError("Input is not a valid edge list") from err
|
||||
|
||||
raise eg.EasyGraphError("Input is not a known data type for conversion.")
|
||||
|
||||
|
||||
def from_dict_of_lists(d, create_using=None):
|
||||
G = eg.empty_graph(0, create_using)
|
||||
G.add_nodes_from(d)
|
||||
if G.is_multigraph() and not G.is_directed():
|
||||
# a dict_of_lists can't show multiedges. BUT for undirected graphs,
|
||||
# each edge shows up twice in the dict_of_lists.
|
||||
# So we need to treat this case separately.
|
||||
seen = {}
|
||||
for node, nbrlist in d.items():
|
||||
for nbr in nbrlist:
|
||||
if nbr not in seen:
|
||||
G.add_edge(node, nbr)
|
||||
seen[node] = 1 # don't allow reverse edge to show up
|
||||
else:
|
||||
G.add_edges_from(
|
||||
((node, nbr) for node, nbrlist in d.items() for nbr in nbrlist)
|
||||
)
|
||||
return G
|
||||
|
||||
|
||||
def from_dict_of_dicts(d, create_using=None, multigraph_input=False):
|
||||
G = eg.empty_graph(0, create_using)
|
||||
G.add_nodes_from(d)
|
||||
# does dict d represent a MultiGraph or MultiDiGraph?
|
||||
if multigraph_input:
|
||||
if G.is_directed():
|
||||
if G.is_multigraph():
|
||||
G.add_edges_from(
|
||||
(u, v, key, data)
|
||||
for u, nbrs in d.items()
|
||||
for v, datadict in nbrs.items()
|
||||
for key, data in datadict.items()
|
||||
)
|
||||
else:
|
||||
G.add_edges_from(
|
||||
(u, v, data)
|
||||
for u, nbrs in d.items()
|
||||
for v, datadict in nbrs.items()
|
||||
for key, data in datadict.items()
|
||||
)
|
||||
else: # Undirected
|
||||
if G.is_multigraph():
|
||||
seen = set() # don't add both directions of undirected graph
|
||||
for u, nbrs in d.items():
|
||||
for v, datadict in nbrs.items():
|
||||
if (u, v) not in seen:
|
||||
G.add_edges_from(
|
||||
(u, v, key, data) for key, data in datadict.items()
|
||||
)
|
||||
seen.add((v, u))
|
||||
else:
|
||||
seen = set() # don't add both directions of undirected graph
|
||||
for u, nbrs in d.items():
|
||||
for v, datadict in nbrs.items():
|
||||
if (u, v) not in seen:
|
||||
G.add_edges_from(
|
||||
(u, v, data) for key, data in datadict.items()
|
||||
)
|
||||
seen.add((v, u))
|
||||
|
||||
else: # not a multigraph to multigraph transfer
|
||||
if G.is_multigraph() and not G.is_directed():
|
||||
# d can have both representations u-v, v-u in dict. Only add one.
|
||||
# We don't need this check for digraphs since we add both directions,
|
||||
# or for Graph() since it is done implicitly (parallel edges not allowed)
|
||||
seen = set()
|
||||
for u, nbrs in d.items():
|
||||
for v, data in nbrs.items():
|
||||
if (u, v) not in seen:
|
||||
G.add_edge(u, v, key=0)
|
||||
G[u][v][0].update(data)
|
||||
seen.add((v, u))
|
||||
else:
|
||||
G.add_edges_from(
|
||||
((u, v, data) for u, nbrs in d.items() for v, data in nbrs.items())
|
||||
)
|
||||
return G
|
||||
|
||||
|
||||
def from_edgelist(edgelist, create_using=None):
|
||||
"""Returns a graph from a list of edges.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
edgelist : list or iterator
|
||||
Edge tuples
|
||||
|
||||
create_using : EasyGraph graph constructor, optional (default=eg.Graph)
|
||||
Graph type to create. If graph instance, then cleared before populated.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> edgelist = [(0, 1)] # single edge (0,1)
|
||||
>>> G = eg.from_edgelist(edgelist)
|
||||
|
||||
or
|
||||
|
||||
>>> G = eg.Graph(edgelist) # use Graph constructor
|
||||
|
||||
"""
|
||||
G = eg.empty_graph(0, create_using)
|
||||
G.add_edges_from(edgelist)
|
||||
return G
|
||||
|
||||
|
||||
def to_networkx(g: "Union[Graph, DiGraph]") -> "Union[nx.Graph, nx.DiGraph]":
|
||||
"""Convert an EasyGraph to a NetworkX graph.
|
||||
|
||||
Args:
|
||||
g (Union[Graph, DiGraph]): An EasyGraph graph
|
||||
|
||||
Raises:
|
||||
ImportError is raised if NetworkX is not installed.
|
||||
|
||||
Returns:
|
||||
Union[nx.Graph, nx.DiGraph]: Converted NetworkX graph
|
||||
"""
|
||||
# if load_func_name in di_load_functions_name:
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError:
|
||||
raise ImportError("NetworkX not found. Please install it.")
|
||||
if g.is_directed():
|
||||
G = nx.DiGraph()
|
||||
else:
|
||||
G = nx.Graph()
|
||||
|
||||
# copy attributes
|
||||
G.graph = deepcopy(g.graph)
|
||||
|
||||
nodes_with_edges = set()
|
||||
for v1, v2, _ in g.edges:
|
||||
G.add_edge(v1, v2)
|
||||
nodes_with_edges.add(v1)
|
||||
nodes_with_edges.add(v2)
|
||||
for node in set(g.nodes) - nodes_with_edges:
|
||||
G.add_node(node)
|
||||
return G
|
||||
|
||||
|
||||
def from_networkx(g: "Union[nx.Graph, nx.DiGraph]") -> "Union[Graph, DiGraph]":
|
||||
"""Convert a NetworkX graph to an EasyGraph graph.
|
||||
|
||||
Args:
|
||||
g (Union[nx.Graph, nx.DiGraph]): A NetworkX graph
|
||||
|
||||
Returns:
|
||||
Union[Graph, DiGraph]: Converted EasyGraph graph
|
||||
"""
|
||||
# try:
|
||||
# import networkx as nx
|
||||
# except ImportError:
|
||||
# raise ImportError("NetworkX not found. Please install it.")
|
||||
if g.is_directed():
|
||||
G = eg.DiGraph()
|
||||
else:
|
||||
G = eg.Graph()
|
||||
|
||||
# copy attributes
|
||||
G.graph = deepcopy(g.graph)
|
||||
|
||||
nodes_with_edges = set()
|
||||
for v1, v2 in g.edges:
|
||||
G.add_edge(v1, v2)
|
||||
nodes_with_edges.add(v1)
|
||||
nodes_with_edges.add(v2)
|
||||
for node in set(g.nodes) - nodes_with_edges:
|
||||
G.add_node(node)
|
||||
return G
|
||||
|
||||
|
||||
def to_dgl(g: "Union[Graph, DiGraph]"):
|
||||
"""Convert an EasyGraph graph to a DGL graph.
|
||||
|
||||
Args:
|
||||
g (Union[Graph, DiGraph]): An EasyGraph graph
|
||||
|
||||
Raises:
|
||||
ImportError: If DGL is not installed.
|
||||
|
||||
Returns:
|
||||
DGLGraph: Converted DGL graph
|
||||
"""
|
||||
try:
|
||||
import dgl
|
||||
except ImportError:
|
||||
raise ImportError("DGL not found. Please install it.")
|
||||
g_nx = to_networkx(g)
|
||||
g_dgl = dgl.from_networkx(g_nx)
|
||||
return g_dgl
|
||||
|
||||
|
||||
def from_dgl(g) -> "Union[Graph, DiGraph]":
|
||||
"""Convert a DGL graph to an EasyGraph graph.
|
||||
|
||||
Args:
|
||||
g (DGLGraph): A DGL graph
|
||||
|
||||
Raises:
|
||||
ImportError: If DGL is not installed.
|
||||
|
||||
Returns:
|
||||
Union[Graph, DiGraph]: Converted EasyGraph graph
|
||||
"""
|
||||
try:
|
||||
import dgl
|
||||
except ImportError:
|
||||
raise ImportError("DGL not found. Please install it.")
|
||||
g_nx = dgl.to_networkx(g)
|
||||
g_eg = from_networkx(g_nx)
|
||||
return g_eg
|
||||
|
||||
|
||||
def to_pyg(
|
||||
G: Any,
|
||||
group_node_attrs: Optional[Union[List[str], all]] = None, # type: ignore
|
||||
group_edge_attrs: Optional[Union[List[str], all]] = None, # type: ignore
|
||||
) -> "torch_geometric.data.Data": # type: ignore
|
||||
r"""Converts a :obj:`easygraph.Graph` or :obj:`easygraph.DiGraph` to a
|
||||
:class:`torch_geometric.data.Data` instance.
|
||||
|
||||
Args:
|
||||
G (easygraph.Graph or easygraph.DiGraph): A easygraph graph.
|
||||
group_node_attrs (List[str] or all, optional): The node attributes to
|
||||
be concatenated and added to :obj:`data.x`. (default: :obj:`None`)
|
||||
group_edge_attrs (List[str] or all, optional): The edge attributes to
|
||||
be concatenated and added to :obj:`data.edge_attr`.
|
||||
(default: :obj:`None`)
|
||||
|
||||
.. note::
|
||||
|
||||
All :attr:`group_node_attrs` and :attr:`group_edge_attrs` values must
|
||||
be numeric.
|
||||
|
||||
Examples:
|
||||
|
||||
>>> import torch_geometric as pyg
|
||||
|
||||
>>> pyg_to_networkx = pyg.utils.convert.to_networkx # type: ignore
|
||||
>>> networkx_to_pyg = pyg.utils.convert.from_networkx # type: ignore
|
||||
>>> Data = pyg.data.Data # type: ignore
|
||||
>>> edge_index = torch.tensor([
|
||||
... [0, 1, 1, 2, 2, 3],
|
||||
... [1, 0, 2, 1, 3, 2],
|
||||
... ])
|
||||
>>> data = Data(edge_index=edge_index, num_nodes=4)
|
||||
>>> g = pyg_to_networkx(data)
|
||||
>>> # A `Data` object is returned
|
||||
>>> to_pyg(g)
|
||||
Data(edge_index=[2, 6], num_nodes=4)
|
||||
"""
|
||||
try:
|
||||
import torch_geometric as pyg
|
||||
|
||||
pyg_to_networkx = pyg.utils.convert.to_networkx # type: ignore
|
||||
networkx_to_pyg = pyg.utils.convert.from_networkx # type: ignore
|
||||
except ImportError:
|
||||
raise ImportError("pytorch_geometric not found. Please install it.")
|
||||
|
||||
g_nx = to_networkx(G)
|
||||
g_pyg = networkx_to_pyg(g_nx, group_node_attrs, group_edge_attrs)
|
||||
return g_pyg
|
||||
|
||||
|
||||
def from_pyg(
|
||||
data: "torch_geometric.data.Data", # type: ignore
|
||||
node_attrs: Optional[Iterable[str]] = None,
|
||||
edge_attrs: Optional[Iterable[str]] = None,
|
||||
graph_attrs: Optional[Iterable[str]] = None,
|
||||
to_undirected: Optional[Union[bool, str]] = False,
|
||||
remove_self_loops: bool = False,
|
||||
) -> Any:
|
||||
r"""Converts a :class:`torch_geometric.data.Data` instance to a
|
||||
:obj:`easygraph.Graph` if :attr:`to_undirected` is set to :obj:`True`, or
|
||||
a directed :obj:`easygraph.DiGraph` otherwise.
|
||||
|
||||
Args:
|
||||
data (torch_geometric.data.Data): The data object.
|
||||
node_attrs (iterable of str, optional): The node attributes to be
|
||||
copied. (default: :obj:`None`)
|
||||
edge_attrs (iterable of str, optional): The edge attributes to be
|
||||
copied. (default: :obj:`None`)
|
||||
graph_attrs (iterable of str, optional): The graph attributes to be
|
||||
copied. (default: :obj:`None`)
|
||||
to_undirected (bool or str, optional): If set to :obj:`True` or
|
||||
"upper", will return a :obj:`easygraph.Graph` instead of a
|
||||
:obj:`easygraph.DiGraph`. The undirected graph will correspond to
|
||||
the upper triangle of the corresponding adjacency matrix.
|
||||
Similarly, if set to "lower", the undirected graph will correspond
|
||||
to the lower triangle of the adjacency matrix. (default:
|
||||
:obj:`False`)
|
||||
remove_self_loops (bool, optional): If set to :obj:`True`, will not
|
||||
include self loops in the resulting graph. (default: :obj:`False`)
|
||||
|
||||
Examples:
|
||||
|
||||
>>> import torch_geometric as pyg
|
||||
|
||||
>>> Data = pyg.data.Data # type: ignore
|
||||
>>> edge_index = torch.tensor([
|
||||
... [0, 1, 1, 2, 2, 3],
|
||||
... [1, 0, 2, 1, 3, 2],
|
||||
... ])
|
||||
>>> data = Data(edge_index=edge_index, num_nodes=4)
|
||||
>>> from_pyg(data)
|
||||
<easygraph.classes.digraph.DiGraph at 0x2713fdb40d0>
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
import torch_geometric as pyg
|
||||
|
||||
pyg_to_networkx = pyg.utils.convert.to_networkx # type: ignore
|
||||
networkx_to_pyg = pyg.utils.convert.from_networkx # type: ignore
|
||||
except ImportError:
|
||||
raise ImportError("pytorch_geometric not found. Please install it.")
|
||||
g_nx = pyg_to_networkx(
|
||||
data, node_attrs, edge_attrs, graph_attrs, to_undirected, remove_self_loops
|
||||
)
|
||||
g_eg = from_networkx(g_nx)
|
||||
return g_eg
|
||||
|
||||
|
||||
def dict_to_hypergraph(data, max_order=None, is_dynamic=False):
|
||||
"""
|
||||
A function to read a file in a standardized JSON format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: dict
|
||||
A dictionary in the hypergraph JSON format
|
||||
max_order: int, optional
|
||||
Maximum order of edges to add to the hypergraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
A Hypergraph object
|
||||
The loaded hypergraph
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If the JSON is not in a format that can be loaded.
|
||||
|
||||
See Also
|
||||
--------
|
||||
read_json
|
||||
|
||||
"""
|
||||
|
||||
timestamp_lst = list()
|
||||
node_data = data["node-data"]
|
||||
node_num = len(node_data)
|
||||
G = eg.Hypergraph(num_v=node_num)
|
||||
try:
|
||||
# print(len(data["node-data"]))
|
||||
for index, dd in data["node-data"].items():
|
||||
id = int(index) - 1
|
||||
G.v_property[id] = dd
|
||||
except KeyError:
|
||||
raise EasyGraphError("Failed to import node attributes.")
|
||||
|
||||
# try:
|
||||
# import time
|
||||
rows = []
|
||||
cols = []
|
||||
edge_flag_dict = {}
|
||||
e_property_dict = data["edge-data"]
|
||||
edge_id = 0
|
||||
for index, edge in data["edge-dict"].items():
|
||||
# print("id:",id)
|
||||
if max_order and len(edge) > max_order + 1:
|
||||
continue
|
||||
|
||||
try:
|
||||
id = int(index)
|
||||
except ValueError as e:
|
||||
raise TypeError(
|
||||
f"Failed to convert the edge with ID {id} to type int."
|
||||
) from e
|
||||
|
||||
try:
|
||||
edge = [int(n) - 1 for n in edge]
|
||||
if tuple(edge) not in edge_flag_dict:
|
||||
edge_flag_dict[tuple(edge)] = 1
|
||||
rows.extend(edge)
|
||||
cols.extend(len(edge) * [edge_id])
|
||||
edge_id += 1
|
||||
|
||||
except ValueError as e:
|
||||
raise TypeError(f"Failed to convert nodes to type int.") from e
|
||||
|
||||
if is_dynamic:
|
||||
G.add_hyperedges(
|
||||
e_list=edge,
|
||||
e_property=e_property_dict[str(id)],
|
||||
group_name=e_property_dict[str(id)]["timestamp"],
|
||||
)
|
||||
|
||||
timestamp_lst.append(e_property_dict[str(id)]["timestamp"])
|
||||
else:
|
||||
G.add_hyperedges(e_list=edge, e_property=e_property_dict[str(id)])
|
||||
G._rows = rows
|
||||
G._cols = cols
|
||||
return G, timestamp_lst
|
||||
@@ -0,0 +1,29 @@
|
||||
try:
|
||||
from .common import compose_pipes
|
||||
from .common import to_bool_tensor
|
||||
from .common import to_long_tensor
|
||||
from .common import to_tensor
|
||||
from .normalize import min_max_scaler
|
||||
from .normalize import norm_ft
|
||||
except:
|
||||
print(
|
||||
"Warning raise in module:datapipe. Please install Pytorch before you use"
|
||||
" functions related to nueral network"
|
||||
)
|
||||
|
||||
from .loader import load_from_json
|
||||
from .loader import load_from_pickle
|
||||
from .loader import load_from_txt
|
||||
|
||||
|
||||
# __all__ = [
|
||||
# "compose_pipes",
|
||||
# "norm_ft",
|
||||
# "min_max_scaler",
|
||||
# "to_tensor",
|
||||
# "to_bool_tensor",
|
||||
# "to_long_tensor",
|
||||
# "load_from_pickle",
|
||||
# "load_from_json",
|
||||
# "load_from_txt",
|
||||
# ]
|
||||
@@ -0,0 +1,106 @@
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
import scipy.sparse
|
||||
import torch
|
||||
|
||||
|
||||
def to_tensor(
|
||||
X: Union[list, np.ndarray, torch.Tensor, scipy.sparse.csr_matrix]
|
||||
) -> torch.Tensor:
|
||||
r"""Convert ``List``, ``numpy.ndarray``, ``scipy.sparse.csr_matrix`` to ``torch.Tensor``.
|
||||
|
||||
Args:
|
||||
``X`` (``Union[List, np.ndarray, torch.Tensor, scipy.sparse.csr_matrix]``): Input.
|
||||
|
||||
Examples:
|
||||
>>> import easygraph.datapipe as dd
|
||||
>>> X = [[0.1, 0.2, 0.5],
|
||||
[0.5, 0.2, 0.3],
|
||||
[0.3, 0.2, 0]]
|
||||
>>> dd.to_tensor(X)
|
||||
tensor([[0.1000, 0.2000, 0.5000],
|
||||
[0.5000, 0.2000, 0.3000],
|
||||
[0.3000, 0.2000, 0.0000]])
|
||||
"""
|
||||
if isinstance(X, list):
|
||||
X = torch.tensor(X)
|
||||
elif isinstance(X, scipy.sparse.csr_matrix):
|
||||
X = X.todense()
|
||||
X = torch.tensor(X)
|
||||
elif isinstance(X, scipy.sparse.coo_matrix):
|
||||
X = X.todense()
|
||||
X = torch.tensor(X)
|
||||
elif isinstance(X, np.ndarray):
|
||||
X = torch.tensor(X)
|
||||
else:
|
||||
X = torch.tensor(X)
|
||||
return X.float()
|
||||
|
||||
|
||||
def to_bool_tensor(X: Union[List, np.ndarray, torch.Tensor]) -> torch.BoolTensor:
|
||||
r"""Convert ``List``, ``numpy.ndarray``, ``torch.Tensor`` to ``torch.BoolTensor``.
|
||||
|
||||
Args:
|
||||
``X`` (``Union[List, np.ndarray, torch.Tensor]``): Input.
|
||||
|
||||
Examples:
|
||||
>>> import easygraph.datapipe as dd
|
||||
>>> X = [[0.1, 0.2, 0.5],
|
||||
[0.5, 0.2, 0.3],
|
||||
[0.3, 0.2, 0]]
|
||||
>>> dd.to_bool_tensor(X)
|
||||
tensor([[ True, True, True],
|
||||
[ True, True, True],
|
||||
[ True, True, False]])
|
||||
"""
|
||||
if isinstance(X, list):
|
||||
X = torch.tensor(X)
|
||||
elif isinstance(X, np.ndarray):
|
||||
X = torch.tensor(X)
|
||||
else:
|
||||
X = torch.tensor(X)
|
||||
return X.bool()
|
||||
|
||||
|
||||
def to_long_tensor(X: Union[List, np.ndarray, torch.Tensor]) -> torch.LongTensor:
|
||||
r"""Convert ``List``, ``numpy.ndarray``, ``torch.Tensor`` to ``torch.LongTensor``.
|
||||
|
||||
Args:
|
||||
``X`` (``Union[List, np.ndarray, torch.Tensor]``): Input.
|
||||
|
||||
Examples:
|
||||
>>> import easygraph.datapipe as dd
|
||||
>>> X = [[1, 2, 5],
|
||||
[5, 2, 3],
|
||||
[3, 2, 0]]
|
||||
>>> dd.to_long_tensor(X)
|
||||
tensor([[1, 2, 5],
|
||||
[5, 2, 3],
|
||||
[3, 2, 0]])
|
||||
"""
|
||||
if isinstance(X, list):
|
||||
X = torch.tensor(X)
|
||||
elif isinstance(X, np.ndarray):
|
||||
X = torch.tensor(X)
|
||||
else:
|
||||
X = torch.tensor(X)
|
||||
return X.long()
|
||||
|
||||
|
||||
def compose_pipes(*pipes: Callable) -> Callable:
|
||||
r"""Compose datapipe functions.
|
||||
|
||||
Args:
|
||||
``pipes`` (``Callable``): Datapipe functions to compose.
|
||||
"""
|
||||
|
||||
def composed_pipes(X: Any) -> torch.Tensor:
|
||||
for pipe in pipes:
|
||||
X = pipe(X)
|
||||
return X
|
||||
|
||||
return composed_pipes
|
||||
@@ -0,0 +1,90 @@
|
||||
import json
|
||||
import pickle as pkl
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
|
||||
def load_from_pickle(
|
||||
file_path: Path, keys: Optional[Union[str, List[str]]] = None, **kwargs
|
||||
):
|
||||
r"""Load data from a pickle file.
|
||||
|
||||
Args:
|
||||
``file_path`` (``Path``): The local path of the file.
|
||||
``keys`` (``Union[str, List[str]]``, optional): The keys of the data. Defaults to ``None``.
|
||||
"""
|
||||
if isinstance(file_path, list):
|
||||
raise ValueError("This function only support loading data from a single file.")
|
||||
with open(file_path, "rb") as f:
|
||||
data = pkl.load(f, **kwargs)
|
||||
if keys is None:
|
||||
return data
|
||||
elif isinstance(keys, str):
|
||||
return data[keys]
|
||||
else:
|
||||
return {key: data[key] for key in keys}
|
||||
|
||||
|
||||
def load_from_json(file_path: Path, **kwargs):
|
||||
r"""Load data from a json file.
|
||||
|
||||
Args:
|
||||
``file_path`` (``Path``): The local path of the file.
|
||||
"""
|
||||
with open(file_path, "r") as f:
|
||||
data = json.load(f, **kwargs)
|
||||
return data
|
||||
|
||||
|
||||
def load_from_txt(
|
||||
file_path: Path,
|
||||
dtype: Union[str, Callable],
|
||||
sep: str = ",| |\t",
|
||||
ignore_header: int = 0,
|
||||
):
|
||||
r"""Load data from a txt file.
|
||||
|
||||
.. note::
|
||||
The separator is a regular expression of ``re`` module. Multiple separators can be separated by ``|``. More details can refer to `re.split <https://docs.python.org/3/library/re.html#re.split>`_.
|
||||
|
||||
Args:
|
||||
``file_path`` (``Path``): The local path of the file.
|
||||
``dtype`` (``Union[str, Callable]``): The data type of the data can be either a string or a callable function.
|
||||
``sep`` (``str``, optional): The separator of each line in the file. Defaults to ``",| |\t"``.
|
||||
``ignore_header`` (``int``, optional): The number of lines to ignore in the header of the file. Defaults to ``0``.
|
||||
"""
|
||||
cast_fun = ret_cast_fun(dtype)
|
||||
file_path = Path(file_path)
|
||||
assert file_path.exists(), f"{file_path} does not exist."
|
||||
data = []
|
||||
with open(file_path, "r") as f:
|
||||
for _ in range(ignore_header):
|
||||
f.readline()
|
||||
data = [
|
||||
list(map(cast_fun, re.split(sep, line.strip()))) for line in f.readlines()
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
def ret_cast_fun(dtype: Union[str, Callable]):
|
||||
r"""Return the cast function of the data type. The supported data types are: ``int``, ``float``, ``str``.
|
||||
|
||||
Args:
|
||||
``dtype`` (``Union[str, Callable]``): The data type of the data can be either a string or a callable function.
|
||||
"""
|
||||
if isinstance(dtype, str):
|
||||
if dtype == "int":
|
||||
return int
|
||||
elif dtype == "float":
|
||||
return float
|
||||
elif dtype == "str":
|
||||
return str
|
||||
else:
|
||||
raise ValueError("dtype must be one of 'int', 'float', 'str'.")
|
||||
else:
|
||||
return dtype
|
||||
@@ -0,0 +1,74 @@
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def norm_ft(X: torch.Tensor, ord: Optional[Union[int, float]] = None) -> torch.Tensor:
|
||||
r"""Normalize the input feature matrix with specified ``ord`` refer to pytorch's `torch.linalg.norm <https://pytorch.org/docs/stable/generated/torch.linalg.norm.html#torch.linalg.norm>`_ function.
|
||||
|
||||
.. note::
|
||||
The input feature matrix is expected to be a 1D vector or a 2D tensor with shape (num_samples, num_features).
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): The input feature.
|
||||
``ord`` (``Union[int, float]``, optional): The order of the norm can be either an ``int``, ``float``. If ``ord`` is ``None``, the norm is computed with the 2-norm. Defaults to ``None``.
|
||||
|
||||
Examples:
|
||||
>>> import easygraph.datapipe as dd
|
||||
>>> import torch
|
||||
>>> X = torch.tensor([
|
||||
[0.1, 0.2, 0.5],
|
||||
[0.5, 0.2, 0.3],
|
||||
[0.3, 0.2, 0]
|
||||
])
|
||||
>>> dd.norm_ft(X)
|
||||
tensor([[0.1826, 0.3651, 0.9129],
|
||||
[0.8111, 0.3244, 0.4867],
|
||||
[0.8321, 0.5547, 0.0000]])
|
||||
"""
|
||||
if X.dim() == 1:
|
||||
X_norm = 1 / torch.linalg.norm(X, ord=ord)
|
||||
X_norm[torch.isinf(X_norm)] = 0
|
||||
return X * X_norm
|
||||
elif X.dim() == 2:
|
||||
X_norm = 1 / torch.linalg.norm(X, ord=ord, dim=1, keepdim=True)
|
||||
X_norm[torch.isinf(X_norm)] = 0
|
||||
return X * X_norm
|
||||
else:
|
||||
raise ValueError(
|
||||
"The input feature matrix is expected to be a 1D verter or a 2D tensor with"
|
||||
" shape (num_samples, num_features)."
|
||||
)
|
||||
|
||||
|
||||
def min_max_scaler(X: torch.Tensor, ft_min: float, ft_max: float) -> torch.Tensor:
|
||||
r"""Normalize the input feature matrix with min-max scaling.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): The input feature.
|
||||
``ft_min`` (``float``): The minimum value of the output feature.
|
||||
``ft_max`` (``float``): The maximum value of the output feature.
|
||||
|
||||
Examples:
|
||||
>>> import easygraph.datapipe as dd
|
||||
>>> import torch
|
||||
>>> X = torch.tensor([
|
||||
[0.1, 0.2, 0.5],
|
||||
[0.5, 0.2, 0.3],
|
||||
[0.3, 0.2, 0.0]
|
||||
])
|
||||
>>> dd.min_max_scaler(X, -1, 1)
|
||||
tensor([[-0.6000, -0.2000, 1.0000],
|
||||
[ 1.0000, -0.2000, 0.2000],
|
||||
[ 0.2000, -0.2000, -1.0000]])
|
||||
"""
|
||||
assert (
|
||||
ft_min < ft_max
|
||||
), "The minimum value of the feature should be less than the maximum value."
|
||||
X_min, X_max = X.min().item(), X.max().item()
|
||||
X_range = X_max - X_min
|
||||
scale_ = (ft_max - ft_min) / X_range
|
||||
min_ = ft_min - X_min * scale_
|
||||
X = X * scale_ + min_
|
||||
return X
|
||||
@@ -0,0 +1,35 @@
|
||||
# risky imports
|
||||
try:
|
||||
from easygraph.datasets.get_sample_graph import *
|
||||
from easygraph.datasets.gnn_benchmark import *
|
||||
from easygraph.datasets.hypergraph.coauthorship import *
|
||||
from easygraph.datasets.hypergraph.contact_primary_school import *
|
||||
from easygraph.datasets.hypergraph.cooking_200 import Cooking200
|
||||
from easygraph.datasets.hypergraph.House_Committees import House_Committees
|
||||
from easygraph.datasets.karate import KarateClubDataset
|
||||
from easygraph.datasets.mathoverflow_answers import mathoverflow_answers
|
||||
|
||||
from .ppi import LegacyPPIDataset
|
||||
from .ppi import PPIDataset
|
||||
except Exception as e:
|
||||
print(
|
||||
" Please install Pytorch before use graph-related datasets and"
|
||||
" hypergraph-related datasets."
|
||||
)
|
||||
|
||||
from .amazon_photo import AmazonPhotoDataset
|
||||
from .arxiv import ArxivHEPTHDataset
|
||||
from .citation_graph import CitationGraphDataset
|
||||
from .citation_graph import CiteseerGraphDataset
|
||||
from .citation_graph import CoraBinary
|
||||
from .citation_graph import CoraGraphDataset
|
||||
from .citation_graph import PubmedGraphDataset
|
||||
from .coauthor import CoauthorCSDataset
|
||||
from .facebook_ego import FacebookEgoNetDataset
|
||||
from .flickr import FlickrDataset
|
||||
from .github import GitHubUsersDataset
|
||||
from .reddit import RedditDataset
|
||||
from .roadnet import RoadNetCADataset
|
||||
from .twitter_ego import TwitterEgoDataset
|
||||
from .web_google import WebGoogleDataset
|
||||
from .wiki_topcats import WikiTopCatsDataset
|
||||
@@ -0,0 +1,110 @@
|
||||
import os
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import data_type_dict
|
||||
from .utils import download
|
||||
from .utils import extract_archive
|
||||
from .utils import tensor
|
||||
|
||||
|
||||
class AmazonPhotoDataset(EasyGraphBuiltinDataset):
|
||||
r"""Amazon Electronics Photo co-purchase graph dataset.
|
||||
|
||||
Nodes represent products, and edges link products frequently co-purchased.
|
||||
Node features are bag-of-words of product reviews. The task is to classify
|
||||
the product category.
|
||||
|
||||
Statistics:
|
||||
|
||||
- Nodes: 7,650
|
||||
- Edges: 119,081
|
||||
- Number of Classes: 8
|
||||
- Features: 745
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str, optional
|
||||
Raw file directory to download/contains the input data directory. Default: None
|
||||
force_reload : bool, optional
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool, optional
|
||||
Whether to print out progress information. Default: True
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~easygraph.Graph` object and returns
|
||||
a transformed version. The :class:`~easygraph.Graph` object will be
|
||||
transformed before every access.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.datasets import AmazonPhotoDataset
|
||||
>>> dataset = AmazonPhotoDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> print(g.number_of_nodes())
|
||||
>>> print(g.number_of_edges())
|
||||
>>> print(g.nodes[0]['feat'].shape)
|
||||
>>> print(g.nodes[0]['label'])
|
||||
>>> print(dataset.num_classes)
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
name = "amazon_photo"
|
||||
url = "https://data.dgl.ai/dataset/amazon_co_buy_photo.zip"
|
||||
super(AmazonPhotoDataset, self).__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
path = os.path.join(self.raw_path, "amazon_co_buy_photo.npz")
|
||||
data = np.load(path)
|
||||
|
||||
adj = sp.csr_matrix(
|
||||
(data["adj_data"], data["adj_indices"], data["adj_indptr"]),
|
||||
shape=data["adj_shape"],
|
||||
)
|
||||
|
||||
features = sp.csr_matrix(
|
||||
(data["attr_data"], data["attr_indices"], data["attr_indptr"]),
|
||||
shape=data["attr_shape"],
|
||||
).todense()
|
||||
|
||||
labels = data["labels"]
|
||||
|
||||
g = eg.Graph()
|
||||
g.add_edges_from(list(zip(*adj.nonzero())))
|
||||
|
||||
for i in range(features.shape[0]):
|
||||
g.add_node(i, feat=np.array(features[i]).squeeze(), label=int(labels[i]))
|
||||
|
||||
self._g = g
|
||||
self._num_classes = len(np.unique(labels))
|
||||
|
||||
if self.verbose:
|
||||
print("Finished loading AmazonPhoto dataset.")
|
||||
print(f" NumNodes: {g.number_of_nodes()}")
|
||||
print(f" NumEdges: {g.number_of_edges()}")
|
||||
print(f" NumFeats: {features.shape[1]}")
|
||||
print(f" NumClasses: {self._num_classes}")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "AmazonPhotoDataset only contains one graph"
|
||||
if self._g is None:
|
||||
raise ValueError("Graph has not been loaded or processed correctly.")
|
||||
return self._g if self._transform is None else self._transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
return self._num_classes
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Arxiv HEP-TH Citation Network
|
||||
|
||||
This dataset represents the citation network of preprints from the High Energy Physics - Theory (HEP-TH) category on arXiv, covering the period from January 1993 to April 2003.
|
||||
|
||||
Each node corresponds to a paper, and a directed edge from paper A to paper B indicates that A cites B.
|
||||
|
||||
No features or labels are included in this dataset.
|
||||
|
||||
Statistics:
|
||||
- Nodes: 27,770
|
||||
- Edges: 352,807
|
||||
- Features: None
|
||||
- Labels: None
|
||||
|
||||
Reference:
|
||||
J. Leskovec, J. Kleinberg and C. Faloutsos, "Graphs over Time: Densification Laws, Shrinking Diameters and Possible Explanations,"
|
||||
in KDD 2005. Dataset: https://snap.stanford.edu/data/cit-HepTh.html
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import download
|
||||
|
||||
|
||||
class ArxivHEPTHDataset(EasyGraphBuiltinDataset):
|
||||
r"""Arxiv HEP-TH citation network dataset.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str, optional
|
||||
Directory to store the raw downloaded files. Default: None
|
||||
force_reload : bool, optional
|
||||
Whether to re-download and process the dataset. Default: False
|
||||
verbose : bool, optional
|
||||
Whether to print detailed processing logs. Default: True
|
||||
transform : callable, optional
|
||||
Optional transform to apply on the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.datasets import ArxivHEPTHDataset
|
||||
>>> dataset = ArxivHEPTHDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> print("Nodes:", g.number_of_nodes())
|
||||
>>> print("Edges:", g.number_of_edges())
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
name = "cit-HepTh"
|
||||
url = "https://snap.stanford.edu/data/cit-HepTh.txt.gz"
|
||||
super(ArxivHEPTHDataset, self).__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def download(self):
|
||||
r"""Download and decompress the .txt.gz file."""
|
||||
compressed_path = os.path.join(self.raw_dir, self.name + ".txt.gz")
|
||||
extracted_path = os.path.join(self.raw_path, self.name + ".txt")
|
||||
|
||||
download(self.url, path=compressed_path)
|
||||
|
||||
if not os.path.exists(self.raw_path):
|
||||
os.makedirs(self.raw_path)
|
||||
|
||||
with gzip.open(compressed_path, "rb") as f_in:
|
||||
with open(extracted_path, "wb") as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
def process(self):
|
||||
graph = eg.DiGraph() # Citation network is directed
|
||||
edge_list_path = os.path.join(self.raw_path, self.name + ".txt")
|
||||
|
||||
with open(edge_list_path, "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
continue
|
||||
u, v = map(int, line.strip().split())
|
||||
graph.add_edge(u, v)
|
||||
|
||||
self._g = graph
|
||||
self._num_nodes = graph.number_of_nodes()
|
||||
self._num_edges = graph.number_of_edges()
|
||||
|
||||
if self.verbose:
|
||||
print("Finished loading Arxiv HEP-TH dataset.")
|
||||
print(f" NumNodes: {self._num_nodes}")
|
||||
print(f" NumEdges: {self._num_edges}")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "ArxivHEPTHDataset only contains one graph"
|
||||
return self._g if self._transform is None else self._transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
@@ -0,0 +1,875 @@
|
||||
"""Cora, citeseer, pubmed dataset."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import pickle as pkl
|
||||
import sys
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import _get_dgl_url
|
||||
from .utils import data_type_dict
|
||||
from .utils import deprecate_property
|
||||
from .utils import generate_mask_tensor
|
||||
from .utils import nonzero_1d
|
||||
from .utils import tensor
|
||||
|
||||
|
||||
def _pickle_load(pkl_file):
|
||||
if sys.version_info > (3, 0):
|
||||
return pkl.load(pkl_file, encoding="latin1")
|
||||
else:
|
||||
return pkl.load(pkl_file)
|
||||
|
||||
|
||||
class CitationGraphDataset(EasyGraphBuiltinDataset):
|
||||
r"""The citation graph dataset, including Cora, CiteSeer and PubMed.
|
||||
Nodes mean authors and edges mean citation relationships.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: str
|
||||
name can be 'Cora', 'CiteSeer' or 'PubMed'.
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: True.
|
||||
reverse_edge : bool
|
||||
Whether to add reverse edges in graph. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~eg.Graph` object and returns
|
||||
a transformed version. The :class:`~eg.Graph` object will be
|
||||
transformed before every access.
|
||||
reorder : bool
|
||||
Whether to reorder the graph using :func:`~eg.reorder_graph`. Default: False.
|
||||
"""
|
||||
|
||||
_urls = {
|
||||
"cora_v2": "dataset/cora_v2.zip",
|
||||
"citeseer": "dataset/citeseer.zip",
|
||||
"pubmed": "dataset/pubmed.zip",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
reverse_edge=True,
|
||||
transform=None,
|
||||
reorder=False,
|
||||
):
|
||||
assert name.lower() in ["cora", "citeseer", "pubmed"]
|
||||
|
||||
# Previously we use the pre-processing in pygcn (https://github.com/tkipf/pygcn)
|
||||
# for Cora, which is slightly different from the one used in the GCN paper
|
||||
if name.lower() == "cora":
|
||||
name = "cora_v2"
|
||||
|
||||
url = _get_dgl_url(self._urls[name])
|
||||
self._reverse_edge = reverse_edge
|
||||
self._reorder = reorder
|
||||
|
||||
super(CitationGraphDataset, self).__init__(
|
||||
name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
"""Loads input data from data directory and reorder graph for better locality
|
||||
|
||||
ind.name.x => the feature vectors of the training instances as scipy.sparse.csr.csr_matrix object;
|
||||
ind.name.tx => the feature vectors of the test instances as scipy.sparse.csr.csr_matrix object;
|
||||
ind.name.allx => the feature vectors of both labeled and unlabeled training instances
|
||||
(a superset of ind.name.x) as scipy.sparse.csr.csr_matrix object;
|
||||
ind.name.y => the one-hot labels of the labeled training instances as numpy.ndarray object;
|
||||
ind.name.ty => the one-hot labels of the test instances as numpy.ndarray object;
|
||||
ind.name.ally => the labels for instances in ind.name.allx as numpy.ndarray object;
|
||||
ind.name.graph => a dict in the format {index: [index_of_neighbor_nodes]} as collections.defaultdict
|
||||
object;
|
||||
ind.name.test.index => the indices of test instances in graph, for the inductive setting as list object.
|
||||
"""
|
||||
root = self.raw_path
|
||||
objnames = ["x", "y", "tx", "ty", "allx", "ally", "graph"]
|
||||
objects = []
|
||||
for i in range(len(objnames)):
|
||||
with open("{}/ind.{}.{}".format(root, self.name, objnames[i]), "rb") as f:
|
||||
objects.append(_pickle_load(f))
|
||||
|
||||
x, y, tx, ty, allx, ally, graph = tuple(objects)
|
||||
test_idx_reorder = _parse_index_file(
|
||||
"{}/ind.{}.test.index".format(root, self.name)
|
||||
)
|
||||
test_idx_range = np.sort(test_idx_reorder)
|
||||
|
||||
if self.name == "citeseer":
|
||||
# Fix CiteSeer dataset (there are some isolated nodes in the graph)
|
||||
# Find isolated nodes, add them as zero-vecs into the right position
|
||||
test_idx_range_full = range(
|
||||
min(test_idx_reorder), max(test_idx_reorder) + 1
|
||||
)
|
||||
tx_extended = sp.lil_matrix((len(test_idx_range_full), x.shape[1]))
|
||||
tx_extended[test_idx_range - min(test_idx_range), :] = tx
|
||||
tx = tx_extended
|
||||
ty_extended = np.zeros((len(test_idx_range_full), y.shape[1]))
|
||||
ty_extended[test_idx_range - min(test_idx_range), :] = ty
|
||||
ty = ty_extended
|
||||
|
||||
features = sp.vstack((allx, tx)).tolil()
|
||||
features[test_idx_reorder, :] = features[test_idx_range, :]
|
||||
|
||||
if self.reverse_edge:
|
||||
g = eg.DiGraph(eg.from_dict_of_lists(graph))
|
||||
# g = from_networkx(graph)
|
||||
else:
|
||||
graph = eg.Graph(eg.from_dict_of_lists(graph))
|
||||
# edges = list(graph.edges())
|
||||
# u, v = map(list, zip(*edges))
|
||||
# g = dgl_graph((u, v))
|
||||
|
||||
onehot_labels = np.vstack((ally, ty))
|
||||
onehot_labels[test_idx_reorder, :] = onehot_labels[test_idx_range, :]
|
||||
labels = np.argmax(onehot_labels, 1)
|
||||
|
||||
idx_test = test_idx_range.tolist()
|
||||
idx_train = range(len(y))
|
||||
idx_val = range(len(y), len(y) + 500)
|
||||
|
||||
train_mask = generate_mask_tensor(_sample_mask(idx_train, labels.shape[0]))
|
||||
val_mask = generate_mask_tensor(_sample_mask(idx_val, labels.shape[0]))
|
||||
test_mask = generate_mask_tensor(_sample_mask(idx_test, labels.shape[0]))
|
||||
|
||||
g.ndata["train_mask"] = train_mask
|
||||
g.ndata["val_mask"] = val_mask
|
||||
g.ndata["test_mask"] = test_mask
|
||||
g.ndata["label"] = tensor(labels)
|
||||
g.ndata["feat"] = tensor(
|
||||
_preprocess_features(features), dtype=data_type_dict()["float32"]
|
||||
)
|
||||
self._num_classes = onehot_labels.shape[1]
|
||||
self._labels = labels
|
||||
# if self._reorder:
|
||||
# self._g = reorder_graph(
|
||||
# g, node_permute_algo='rcmk', edge_permute_algo='dst', store_ids=False)
|
||||
# else:
|
||||
self._g = g
|
||||
|
||||
if self.verbose:
|
||||
print("Finished data loading and preprocessing.")
|
||||
print(" NumNodes: {}".format(self._g.number_of_nodes()))
|
||||
print(" NumEdges: {}".format(self._g.number_of_edges()))
|
||||
print(" NumFeats: {}".format(self._g.ndata["feat"].shape[1]))
|
||||
print(" NumClasses: {}".format(self.num_classes))
|
||||
print(
|
||||
" NumTrainingSamples: {}".format(
|
||||
nonzero_1d(self._g.ndata["train_mask"]).shape[0]
|
||||
)
|
||||
)
|
||||
print(
|
||||
" NumValidationSamples: {}".format(
|
||||
nonzero_1d(self._g.ndata["val_mask"]).shape[0]
|
||||
)
|
||||
)
|
||||
print(
|
||||
" NumTestSamples: {}".format(
|
||||
nonzero_1d(self._g.ndata["test_mask"]).shape[0]
|
||||
)
|
||||
)
|
||||
|
||||
def has_cache(self):
|
||||
graph_path = os.path.join(self.save_path, self.save_name + ".bin")
|
||||
info_path = os.path.join(self.save_path, self.save_name + ".pkl")
|
||||
if os.path.exists(graph_path) and os.path.exists(info_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# def save(self):
|
||||
# """save the graph list and the labels"""
|
||||
# graph_path = os.path.join(self.save_path,
|
||||
# self.save_name + '.bin')
|
||||
# info_path = os.path.join(self.save_path,
|
||||
# self.save_name + '.pkl')
|
||||
# save_graphs(str(graph_path), self._g)
|
||||
# save_info(str(info_path), {'num_classes': self.num_classes})
|
||||
#
|
||||
# def load(self):
|
||||
# graph_path = os.path.join(self.save_path,
|
||||
# self.save_name + '.bin')
|
||||
# info_path = os.path.join(self.save_path,
|
||||
# self.save_name + '.pkl')
|
||||
# graphs, _ = load_graphs(str(graph_path))
|
||||
#
|
||||
# info = load_info(str(info_path))
|
||||
# graph = graphs[0]
|
||||
# self._g = graph
|
||||
# # for compatibility
|
||||
# graph = graph.clone()
|
||||
# graph.ndata.pop('train_mask')
|
||||
# graph.ndata.pop('val_mask')
|
||||
# graph.ndata.pop('test_mask')
|
||||
# graph.ndata.pop('feat')
|
||||
# graph.ndata.pop('label')
|
||||
# graph = to_networkx(graph)
|
||||
#
|
||||
# self._num_classes = info['num_classes']
|
||||
# self._g.ndata['train_mask'] = generate_mask_tensor(F.asnumpy(self._g.ndata['train_mask']))
|
||||
# self._g.ndata['val_mask'] = generate_mask_tensor(F.asnumpy(self._g.ndata['val_mask']))
|
||||
# self._g.ndata['test_mask'] = generate_mask_tensor(F.asnumpy(self._g.ndata['test_mask']))
|
||||
# # hack for mxnet compatibility
|
||||
#
|
||||
# if self.verbose:
|
||||
# print(' NumNodes: {}'.format(self._g.number_of_nodes()))
|
||||
# print(' NumEdges: {}'.format(self._g.number_of_edges()))
|
||||
# print(' NumFeats: {}'.format(self._g.ndata['feat'].shape[1]))
|
||||
# print(' NumClasses: {}'.format(self.num_classes))
|
||||
# print(' NumTrainingSamples: {}'.format(
|
||||
# F.nonzero_1d(self._g.ndata['train_mask']).shape[0]))
|
||||
# print(' NumValidationSamples: {}'.format(
|
||||
# F.nonzero_1d(self._g.ndata['val_mask']).shape[0]))
|
||||
# print(' NumTestSamples: {}'.format(
|
||||
# F.nonzero_1d(self._g.ndata['test_mask']).shape[0]))
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
if self._transform is None:
|
||||
return self._g
|
||||
else:
|
||||
return self._transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
@property
|
||||
def save_name(self):
|
||||
return self.name + "_dgl_graph"
|
||||
|
||||
@property
|
||||
def num_labels(self):
|
||||
deprecate_property("dataset.num_labels", "dataset.num_classes")
|
||||
return self.num_classes
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
return self._num_classes
|
||||
|
||||
""" Citation graph is used in many examples
|
||||
We preserve these properties for compatibility.
|
||||
"""
|
||||
|
||||
@property
|
||||
def reverse_edge(self):
|
||||
return self._reverse_edge
|
||||
|
||||
|
||||
def _preprocess_features(features):
|
||||
"""Row-normalize feature matrix and convert to tuple representation"""
|
||||
rowsum = np.asarray(features.sum(1))
|
||||
r_inv = np.power(rowsum, -1).flatten()
|
||||
r_inv[np.isinf(r_inv)] = 0.0
|
||||
r_mat_inv = sp.diags(r_inv)
|
||||
features = r_mat_inv.dot(features)
|
||||
return np.asarray(features.todense())
|
||||
|
||||
|
||||
def _parse_index_file(filename):
|
||||
"""Parse index file."""
|
||||
index = []
|
||||
for line in open(filename):
|
||||
index.append(int(line.strip()))
|
||||
return index
|
||||
|
||||
|
||||
def _sample_mask(idx, l):
|
||||
"""Create mask."""
|
||||
mask = np.zeros(l)
|
||||
mask[idx] = 1
|
||||
return mask
|
||||
|
||||
|
||||
class CoraGraphDataset(CitationGraphDataset):
|
||||
r"""Cora citation network dataset.
|
||||
|
||||
Nodes mean paper and edges mean citation
|
||||
relationships. Each node has a predefined
|
||||
feature with 1433 dimensions. The dataset is
|
||||
designed for the node classification task.
|
||||
The task is to predict the category of
|
||||
certain paper.
|
||||
|
||||
Statistics:
|
||||
|
||||
- Nodes: 2708
|
||||
- Edges: 10556
|
||||
- Number of Classes: 7
|
||||
- Label split:
|
||||
|
||||
- Train: 140
|
||||
- Valid: 500
|
||||
- Test: 1000
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: True.
|
||||
reverse_edge : bool
|
||||
Whether to add reverse edges in graph. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
reorder : bool
|
||||
Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
num_classes: int
|
||||
Number of label classes
|
||||
|
||||
Notes
|
||||
-----
|
||||
The node feature is row-normalized.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> dataset = CoraGraphDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> num_class = dataset.num_classes
|
||||
>>>
|
||||
>>> # get node feature
|
||||
>>> feat = g.ndata['feat']
|
||||
>>>
|
||||
>>> # get data split
|
||||
>>> train_mask = g.ndata['train_mask']
|
||||
>>> val_mask = g.ndata['val_mask']
|
||||
>>> test_mask = g.ndata['test_mask']
|
||||
>>>
|
||||
>>> # get labels
|
||||
>>> label = g.ndata['label']
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
reverse_edge=True,
|
||||
transform=None,
|
||||
reorder=False,
|
||||
):
|
||||
name = "cora"
|
||||
|
||||
super(CoraGraphDataset, self).__init__(
|
||||
name, raw_dir, force_reload, verbose, reverse_edge, transform, reorder
|
||||
)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
r"""Gets the graph object
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
idx: int
|
||||
Item index, CoraGraphDataset has only one graph object
|
||||
|
||||
Return
|
||||
------
|
||||
:class:`dgl.DGLGraph`
|
||||
|
||||
graph structure, node features and labels.
|
||||
|
||||
- ``ndata['train_mask']``: mask for training node set
|
||||
- ``ndata['val_mask']``: mask for validation node set
|
||||
- ``ndata['test_mask']``: mask for test node set
|
||||
- ``ndata['feat']``: node feature
|
||||
- ``ndata['label']``: ground truth labels
|
||||
"""
|
||||
return super(CoraGraphDataset, self).__getitem__(idx)
|
||||
|
||||
def __len__(self):
|
||||
r"""The number of graphs in the dataset."""
|
||||
return super(CoraGraphDataset, self).__len__()
|
||||
|
||||
|
||||
class CiteseerGraphDataset(CitationGraphDataset):
|
||||
r"""Citeseer citation network dataset.
|
||||
|
||||
Nodes mean scientific publications and edges
|
||||
mean citation relationships. Each node has a
|
||||
predefined feature with 3703 dimensions. The
|
||||
dataset is designed for the node classification
|
||||
task. The task is to predict the category of
|
||||
certain publication.
|
||||
|
||||
Statistics:
|
||||
|
||||
- Nodes: 3327
|
||||
- Edges: 9228
|
||||
- Number of Classes: 6
|
||||
- Label Split:
|
||||
|
||||
- Train: 120
|
||||
- Valid: 500
|
||||
- Test: 1000
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: True.
|
||||
reverse_edge : bool
|
||||
Whether to add reverse edges in graph. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
reorder : bool
|
||||
Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
num_classes: int
|
||||
Number of label classes
|
||||
|
||||
Notes
|
||||
-----
|
||||
The node feature is row-normalized.
|
||||
|
||||
In citeseer dataset, there are some isolated nodes in the graph.
|
||||
These isolated nodes are added as zero-vecs into the right position.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> dataset = CiteseerGraphDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> num_class = dataset.num_classes
|
||||
>>>
|
||||
>>> # get node feature
|
||||
>>> feat = g.ndata['feat']
|
||||
>>>
|
||||
>>> # get data split
|
||||
>>> train_mask = g.ndata['train_mask']
|
||||
>>> val_mask = g.ndata['val_mask']
|
||||
>>> test_mask = g.ndata['test_mask']
|
||||
>>>
|
||||
>>> # get labels
|
||||
>>> label = g.ndata['label']
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
reverse_edge=True,
|
||||
transform=None,
|
||||
reorder=False,
|
||||
):
|
||||
name = "citeseer"
|
||||
|
||||
super(CiteseerGraphDataset, self).__init__(
|
||||
name, raw_dir, force_reload, verbose, reverse_edge, transform, reorder
|
||||
)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
r"""Gets the graph object
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
idx: int
|
||||
Item index, CiteseerGraphDataset has only one graph object
|
||||
|
||||
Return
|
||||
------
|
||||
:class:`dgl.DGLGraph`
|
||||
|
||||
graph structure, node features and labels.
|
||||
|
||||
- ``ndata['train_mask']``: mask for training node set
|
||||
- ``ndata['val_mask']``: mask for validation node set
|
||||
- ``ndata['test_mask']``: mask for test node set
|
||||
- ``ndata['feat']``: node feature
|
||||
- ``ndata['label']``: ground truth labels
|
||||
"""
|
||||
return super(CiteseerGraphDataset, self).__getitem__(idx)
|
||||
|
||||
def __len__(self):
|
||||
r"""The number of graphs in the dataset."""
|
||||
return super(CiteseerGraphDataset, self).__len__()
|
||||
|
||||
|
||||
class PubmedGraphDataset(CitationGraphDataset):
|
||||
r"""Pubmed citation network dataset.
|
||||
|
||||
Nodes mean scientific publications and edges
|
||||
mean citation relationships. Each node has a
|
||||
predefined feature with 500 dimensions. The
|
||||
dataset is designed for the node classification
|
||||
task. The task is to predict the category of
|
||||
certain publication.
|
||||
|
||||
Statistics:
|
||||
|
||||
- Nodes: 19717
|
||||
- Edges: 88651
|
||||
- Number of Classes: 3
|
||||
- Label Split:
|
||||
|
||||
- Train: 60
|
||||
- Valid: 500
|
||||
- Test: 1000
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: True.
|
||||
reverse_edge : bool
|
||||
Whether to add reverse edges in graph. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
reorder : bool
|
||||
Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
num_classes: int
|
||||
Number of label classes
|
||||
|
||||
Notes
|
||||
-----
|
||||
The node feature is row-normalized.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> dataset = PubmedGraphDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> num_class = dataset.num_of_class
|
||||
>>>
|
||||
>>> # get node feature
|
||||
>>> feat = g.ndata['feat']
|
||||
>>>
|
||||
>>> # get data split
|
||||
>>> train_mask = g.ndata['train_mask']
|
||||
>>> val_mask = g.ndata['val_mask']
|
||||
>>> test_mask = g.ndata['test_mask']
|
||||
>>>
|
||||
>>> # get labels
|
||||
>>> label = g.ndata['label']
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
reverse_edge=True,
|
||||
transform=None,
|
||||
reorder=False,
|
||||
):
|
||||
name = "pubmed"
|
||||
|
||||
super(PubmedGraphDataset, self).__init__(
|
||||
name, raw_dir, force_reload, verbose, reverse_edge, transform, reorder
|
||||
)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
r"""Gets the graph object
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
idx: int
|
||||
Item index, PubmedGraphDataset has only one graph object
|
||||
|
||||
Return
|
||||
------
|
||||
:class:`dgl.DGLGraph`
|
||||
|
||||
graph structure, node features and labels.
|
||||
|
||||
- ``ndata['train_mask']``: mask for training node set
|
||||
- ``ndata['val_mask']``: mask for validation node set
|
||||
- ``ndata['test_mask']``: mask for test node set
|
||||
- ``ndata['feat']``: node feature
|
||||
- ``ndata['label']``: ground truth labels
|
||||
"""
|
||||
return super(PubmedGraphDataset, self).__getitem__(idx)
|
||||
|
||||
def __len__(self):
|
||||
r"""The number of graphs in the dataset."""
|
||||
return super(PubmedGraphDataset, self).__len__()
|
||||
|
||||
|
||||
def load_cora(
|
||||
raw_dir=None, force_reload=False, verbose=True, reverse_edge=True, transform=None
|
||||
):
|
||||
"""Get CoraGraphDataset
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: True.
|
||||
reverse_edge : bool
|
||||
Whether to add reverse edges in graph. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
|
||||
Return
|
||||
-------
|
||||
CoraGraphDataset
|
||||
"""
|
||||
data = CoraGraphDataset(raw_dir, force_reload, verbose, reverse_edge, transform)
|
||||
return data
|
||||
|
||||
|
||||
def load_citeseer(
|
||||
raw_dir=None, force_reload=False, verbose=True, reverse_edge=True, transform=None
|
||||
):
|
||||
"""Get CiteseerGraphDataset
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: True.
|
||||
reverse_edge : bool
|
||||
Whether to add reverse edges in graph. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
|
||||
Return
|
||||
-------
|
||||
CiteseerGraphDataset
|
||||
"""
|
||||
data = CiteseerGraphDataset(raw_dir, force_reload, verbose, reverse_edge, transform)
|
||||
return data
|
||||
|
||||
|
||||
def load_pubmed(
|
||||
raw_dir=None, force_reload=False, verbose=True, reverse_edge=True, transform=None
|
||||
):
|
||||
"""Get PubmedGraphDataset
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: True.
|
||||
reverse_edge : bool
|
||||
Whether to add reverse edges in graph. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
|
||||
Return
|
||||
-------
|
||||
PubmedGraphDataset
|
||||
"""
|
||||
data = PubmedGraphDataset(raw_dir, force_reload, verbose, reverse_edge, transform)
|
||||
return data
|
||||
|
||||
|
||||
class CoraBinary(EasyGraphBuiltinDataset):
|
||||
"""A mini-dataset for binary classification task using Cora.
|
||||
|
||||
After loaded, it has following members:
|
||||
|
||||
graphs : list of :class:`~dgl.DGLGraph`
|
||||
pmpds : list of :class:`scipy.sparse.coo_matrix`
|
||||
labels : list of :class:`numpy.ndarray`
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose: bool
|
||||
Whether to print out progress information. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
name = "cora_binary"
|
||||
url = _get_dgl_url("dataset/cora_binary.zip")
|
||||
super(CoraBinary, self).__init__(
|
||||
name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
root = self.raw_path
|
||||
# load graphs
|
||||
self.graphs = []
|
||||
with open("{}/graphs.txt".format(root), "r") as f:
|
||||
elist = []
|
||||
for line in f.readlines():
|
||||
if line.startswith("graph"):
|
||||
if len(elist) != 0:
|
||||
self.graphs.append(Graph(elist))
|
||||
elist = []
|
||||
else:
|
||||
u, v = line.strip().split(" ")
|
||||
elist.append((int(u), int(v)))
|
||||
if len(elist) != 0:
|
||||
self.graphs.append(Graph(tuple(zip(*elist))))
|
||||
with open("{}/pmpds.pkl".format(root), "rb") as f:
|
||||
self.pmpds = _pickle_load(f)
|
||||
self.labels = []
|
||||
with open("{}/labels.txt".format(root), "r") as f:
|
||||
cur = []
|
||||
for line in f.readlines():
|
||||
if line.startswith("graph"):
|
||||
if len(cur) != 0:
|
||||
self.labels.append(np.asarray(cur))
|
||||
cur = []
|
||||
else:
|
||||
cur.append(int(line.strip()))
|
||||
if len(cur) != 0:
|
||||
self.labels.append(np.asarray(cur))
|
||||
# sanity check
|
||||
assert len(self.graphs) == len(self.pmpds)
|
||||
assert len(self.graphs) == len(self.labels)
|
||||
|
||||
def has_cache(self):
|
||||
graph_path = os.path.join(self.save_path, self.save_name + ".bin")
|
||||
if os.path.exists(graph_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# def save(self):
|
||||
# """save the graph list and the labels"""
|
||||
# graph_path = os.path.join(self.save_path,
|
||||
# self.save_name + '.bin')
|
||||
# labels = {}
|
||||
# for i, label in enumerate(self.labels):
|
||||
# labels['{}'.format(i)] = F.tensor(label)
|
||||
# save_graphs(str(graph_path), self.graphs, labels)
|
||||
# if self.verbose:
|
||||
# print('Done saving data into cached files.')
|
||||
#
|
||||
# def load(self):
|
||||
# graph_path = os.path.join(self.save_path,
|
||||
# self.save_name + '.bin')
|
||||
# self.graphs, labels = load_graphs(str(graph_path))
|
||||
#
|
||||
# self.labels = []
|
||||
# for i in range(len(labels)):
|
||||
# self.labels.append(F.asnumpy(labels['{}'.format(i)]))
|
||||
# # load pmpds under self.raw_path
|
||||
# with open("{}/pmpds.pkl".format(self.raw_path), 'rb') as f:
|
||||
# self.pmpds = _pickle_load(f)
|
||||
# if self.verbose:
|
||||
# print('Done loading data into cached files.')
|
||||
# # sanity check
|
||||
# assert len(self.graphs) == len(self.pmpds)
|
||||
# assert len(self.graphs) == len(self.labels)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.graphs)
|
||||
|
||||
def __getitem__(self, i):
|
||||
r"""Gets the idx-th sample.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
idx : int
|
||||
The sample index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(dgl.DGLGraph, scipy.sparse.coo_matrix, int)
|
||||
The graph, scipy sparse coo_matrix and its label.
|
||||
"""
|
||||
if self._transform is None:
|
||||
g = self.graphs[i]
|
||||
else:
|
||||
g = self._transform(self.graphs[i])
|
||||
return (g, self.pmpds[i], self.labels[i])
|
||||
|
||||
@property
|
||||
def save_name(self):
|
||||
return self.name + "_dgl_graph"
|
||||
|
||||
# @staticmethod
|
||||
# def collate_fn(cur):
|
||||
# graphs, pmpds, labels = zip(*cur)
|
||||
# batched_graphs = batch.batch(graphs)
|
||||
# batched_pmpds = sp.block_diag(pmpds)
|
||||
# batched_labels = np.concatenate(labels, axis=0)
|
||||
# return batched_graphs, batched_pmpds, batched_labels
|
||||
|
||||
|
||||
def _normalize(mx):
|
||||
"""Row-normalize sparse matrix"""
|
||||
rowsum = np.asarray(mx.sum(1))
|
||||
r_inv = np.power(rowsum, -1).flatten()
|
||||
r_inv[np.isinf(r_inv)] = 0.0
|
||||
r_mat_inv = sp.diags(r_inv)
|
||||
mx = r_mat_inv.dot(mx)
|
||||
return mx
|
||||
|
||||
|
||||
def _encode_onehot(labels):
|
||||
classes = list(sorted(set(labels)))
|
||||
classes_dict = {c: np.identity(len(classes))[i, :] for i, c in enumerate(classes)}
|
||||
labels_onehot = np.asarray(list(map(classes_dict.get, labels)), dtype=np.int32)
|
||||
return labels_onehot
|
||||
@@ -0,0 +1,118 @@
|
||||
"""CoauthorCS Dataset
|
||||
|
||||
This dataset contains a co-authorship network of authors who submitted papers to CS category.
|
||||
Each node represents an author and edges represent co-authorships.
|
||||
Node features are bag-of-words representations of keywords in the author's papers.
|
||||
The task is node classification, with labels indicating the primary field of study.
|
||||
|
||||
Statistics:
|
||||
- Nodes: 18333
|
||||
- Edges: 81894
|
||||
- Feature Dim: 6805
|
||||
- Classes: 15
|
||||
|
||||
Source: https://github.com/dmlc/dgl/tree/master/examples/pytorch/cluster_gcn
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import data_type_dict
|
||||
from .utils import download
|
||||
from .utils import extract_archive
|
||||
from .utils import tensor
|
||||
|
||||
|
||||
class CoauthorCSDataset(EasyGraphBuiltinDataset):
|
||||
r"""CoauthorCS citation network dataset.
|
||||
|
||||
Nodes are authors, and edges indicate co-authorship relationships. Each node
|
||||
has a bag-of-words feature vector and a label denoting the primary research field.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str, optional
|
||||
Directory to store the raw downloaded files. Default: None
|
||||
force_reload : bool, optional
|
||||
Whether to re-download and process the dataset. Default: False
|
||||
verbose : bool, optional
|
||||
Whether to print detailed processing logs. Default: True
|
||||
transform : callable, optional
|
||||
Transform to apply to the graph on access.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.datasets import CoauthorCSDataset
|
||||
>>> dataset = CoauthorCSDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> print("Nodes:", g.number_of_nodes())
|
||||
>>> print("Edges:", g.number_of_edges())
|
||||
>>> print("Feature shape:", g.nodes[0]['feat'].shape)
|
||||
>>> print("Label:", g.nodes[0]['label'])
|
||||
>>> print("Number of classes:", dataset.num_classes)
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
name = "coauthor_cs"
|
||||
url = "https://data.dgl.ai/dataset/coauthor_cs.zip"
|
||||
super(CoauthorCSDataset, self).__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
path = os.path.join(self.raw_path, "coauthor_cs.npz")
|
||||
data = np.load(path)
|
||||
|
||||
# Reconstruct adjacency matrix
|
||||
adj = sp.csr_matrix(
|
||||
(data["adj_data"], data["adj_indices"], data["adj_indptr"]),
|
||||
shape=data["adj_shape"],
|
||||
)
|
||||
|
||||
# Reconstruct feature matrix
|
||||
features = sp.csr_matrix(
|
||||
(data["attr_data"], data["attr_indices"], data["attr_indptr"]),
|
||||
shape=data["attr_shape"],
|
||||
).todense()
|
||||
|
||||
labels = data["labels"]
|
||||
|
||||
g = eg.Graph()
|
||||
g.add_edges_from(list(zip(*adj.nonzero())))
|
||||
|
||||
for i in range(features.shape[0]):
|
||||
g.add_node(i, feat=np.array(features[i]).squeeze(), label=int(labels[i]))
|
||||
|
||||
self._g = g
|
||||
self._num_classes = len(np.unique(labels))
|
||||
|
||||
if self.verbose:
|
||||
print("Finished loading CoauthorCS dataset.")
|
||||
print(f" NumNodes: {g.number_of_nodes()}")
|
||||
print(f" NumEdges: {g.number_of_edges()}")
|
||||
print(f" NumFeats: {features.shape[1]}")
|
||||
print(f" NumClasses: {self._num_classes}")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "CoauthorCSDataset only contains one graph"
|
||||
if self._g is None:
|
||||
raise ValueError("Graph has not been loaded or processed correctly.")
|
||||
return self._g if self._transform is None else self._transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
return self._num_classes
|
||||
@@ -0,0 +1,4 @@
|
||||
from .email_enron import *
|
||||
from .email_eu import *
|
||||
from .hospital_lyon import *
|
||||
from .load_dataset import *
|
||||
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from easygraph.convert import dict_to_hypergraph
|
||||
from easygraph.datasets.dynamic.load_dataset import request_json_from_url
|
||||
from easygraph.datasets.graph_dataset_base import EasyGraphDataset
|
||||
from easygraph.datasets.utils import _get_eg_url
|
||||
from easygraph.datasets.utils import tensor
|
||||
|
||||
|
||||
class Email_Enron(EasyGraphDataset):
|
||||
_urls = {
|
||||
"email-enron": (
|
||||
"easygraph-data-email-enron/-/raw/main/email-enron.json?inline=false"
|
||||
),
|
||||
"email-eu": "easygraph-data-email-eu/-/raw/main/email-eu.json?inline=false",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
transform=None,
|
||||
save_dir="./",
|
||||
):
|
||||
name = "email-enron"
|
||||
self.url = _get_eg_url(self._urls[name])
|
||||
super(Email_Enron, self).__init__(
|
||||
name=name,
|
||||
url=self.url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
save_dir=save_dir,
|
||||
)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def save_name(self):
|
||||
return self.name
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
if self._transform is None:
|
||||
return self._g
|
||||
else:
|
||||
return self._transform(self._g)
|
||||
|
||||
def load(self):
|
||||
graph_path = os.path.join(self.save_path, self.save_name + ".json")
|
||||
with open(graph_path, "r") as f:
|
||||
self.load_data = json.load(f)
|
||||
|
||||
def has_cache(self):
|
||||
graph_path = os.path.join(self.save_path, self.save_name + ".json")
|
||||
if os.path.exists(graph_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
def download(self):
|
||||
if self.has_cache():
|
||||
self.load()
|
||||
else:
|
||||
root = self.raw_dir
|
||||
data = request_json_from_url(self.url)
|
||||
with open(os.path.join(root, self.save_name + ".json"), "w") as f:
|
||||
json.dump(data, f)
|
||||
self.load_data = data
|
||||
|
||||
def process(self):
|
||||
"""Loads input data from data directory and transfer to target graph for better analysis"""
|
||||
|
||||
self._g, edge_feature_list = dict_to_hypergraph(self.load_data, is_dynamic=True)
|
||||
|
||||
self._g.ndata["hyperedge_feature"] = tensor(
|
||||
range(1, len(edge_feature_list) + 1)
|
||||
)
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = value
|
||||
@@ -0,0 +1,81 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from easygraph.convert import dict_to_hypergraph
|
||||
from easygraph.datasets.dynamic.load_dataset import request_json_from_url
|
||||
from easygraph.datasets.graph_dataset_base import EasyGraphDataset
|
||||
from easygraph.datasets.utils import _get_eg_url
|
||||
from easygraph.datasets.utils import tensor
|
||||
|
||||
|
||||
class Email_Eu(EasyGraphDataset):
|
||||
_urls = {
|
||||
"email-eu": "easygraph-data-email-eu/-/raw/main/email-eu.json?inline=false",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
transform=None,
|
||||
save_dir="./",
|
||||
):
|
||||
name = "email-eu"
|
||||
self.url = _get_eg_url(self._urls[name])
|
||||
super(Email_Eu, self).__init__(
|
||||
name=name,
|
||||
url=self.url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
save_dir=save_dir,
|
||||
)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def save_name(self):
|
||||
return self.name
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
if self._transform is None:
|
||||
return self._g
|
||||
else:
|
||||
return self._transform(self._g)
|
||||
|
||||
def load(self):
|
||||
graph_path = os.path.join(self.save_path, self.save_name + ".json")
|
||||
with open(graph_path, "r") as f:
|
||||
self.load_data = json.load(f)
|
||||
|
||||
def has_cache(self):
|
||||
graph_path = os.path.join(self.save_path, self.save_name + ".json")
|
||||
if os.path.exists(graph_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
def download(self):
|
||||
if self.has_cache():
|
||||
self.load()
|
||||
else:
|
||||
root = self.raw_dir
|
||||
data = request_json_from_url(self.url)
|
||||
with open(os.path.join(root, self.save_name + ".json"), "w") as f:
|
||||
json.dump(data, f)
|
||||
self.load_data = data
|
||||
|
||||
def process(self):
|
||||
"""Loads input data from data directory and transfer to target graph for better analysis"""
|
||||
self._g, edge_feature_list = dict_to_hypergraph(self.load_data, is_dynamic=True)
|
||||
self._g.ndata["hyperedge_feature"] = tensor(
|
||||
range(1, len(edge_feature_list) + 1)
|
||||
)
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = value
|
||||
@@ -0,0 +1,133 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from easygraph.classes.hypergraph import Hypergraph
|
||||
from easygraph.datasets.dynamic.load_dataset import request_json_from_url
|
||||
from easygraph.datasets.graph_dataset_base import EasyGraphDataset
|
||||
from easygraph.datasets.utils import _get_eg_url
|
||||
from easygraph.datasets.utils import tensor
|
||||
|
||||
|
||||
class Hospital_Lyon(EasyGraphDataset):
|
||||
_urls = {
|
||||
"hospital_lyon": (
|
||||
"easygraph-data-hospital-lyon/-/raw/main/hospital-lyon.json?ref_type=heads&inline=false"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
transform=None,
|
||||
save_dir="./",
|
||||
):
|
||||
name = "hospital_lyon"
|
||||
self.url = _get_eg_url(self._urls[name])
|
||||
super(Hospital_Lyon, self).__init__(
|
||||
name=name,
|
||||
url=self.url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
save_dir=save_dir,
|
||||
)
|
||||
|
||||
def preprocess(self, data, max_order=None, is_dynamic=True):
|
||||
# The index of the nodes in this dataset are not continuous and therefore require special processing
|
||||
timestamp_lst = list()
|
||||
node_data = data["node-data"]
|
||||
node_num = len(node_data)
|
||||
G = Hypergraph(num_v=node_num)
|
||||
id = 0
|
||||
name_dict = {}
|
||||
for k, v in data["node-data"].items():
|
||||
name_dict[k] = id
|
||||
v["name"] = k
|
||||
G.v_property[id] = v
|
||||
id = id + 1
|
||||
e_property_dict = data["edge-data"]
|
||||
rows = []
|
||||
cols = []
|
||||
edge_flag_dict = {}
|
||||
edge_id = 0
|
||||
for id, edge in data["edge-dict"].items():
|
||||
if max_order and len(edge) > max_order + 1:
|
||||
continue
|
||||
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError as e:
|
||||
raise TypeError(
|
||||
f"Failed to convert the edge with ID {id} to type int."
|
||||
) from e
|
||||
|
||||
try:
|
||||
edge = [name_dict[n] for n in edge]
|
||||
rows.extend(edge)
|
||||
cols.extend(len(edge) * [edge_id])
|
||||
edge_id += 1
|
||||
except ValueError as e:
|
||||
raise TypeError(f"Failed to convert nodes to type int.") from e
|
||||
if is_dynamic:
|
||||
G.add_hyperedges(
|
||||
e_list=edge,
|
||||
e_property=e_property_dict[str(id)],
|
||||
group_name=e_property_dict[str(id)]["timestamp"],
|
||||
)
|
||||
timestamp_lst.append(e_property_dict[str(id)]["timestamp"])
|
||||
else:
|
||||
G.add_hyperedges(e_list=edge, e_property=e_property_dict[str(id)])
|
||||
G._rows = rows
|
||||
G._cols = cols
|
||||
return G, timestamp_lst
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def save_name(self):
|
||||
return self.name
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
if self._transform is None:
|
||||
return self._g
|
||||
else:
|
||||
return self._transform(self._g)
|
||||
|
||||
def load(self):
|
||||
graph_path = os.path.join(self.save_path, self.save_name + ".json")
|
||||
with open(graph_path, "r") as f:
|
||||
self.load_data = json.load(f)
|
||||
|
||||
def has_cache(self):
|
||||
graph_path = os.path.join(self.save_path, self.save_name + ".json")
|
||||
if os.path.exists(graph_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
def download(self):
|
||||
if self.has_cache():
|
||||
self.load()
|
||||
else:
|
||||
root = self.raw_dir
|
||||
data = request_json_from_url(self.url)
|
||||
with open(os.path.join(root, self.save_name + ".json"), "w") as f:
|
||||
json.dump(data, f)
|
||||
self.load_data = data
|
||||
|
||||
def process(self):
|
||||
"""Loads input data from data directory and transfer to target graph for better analysis"""
|
||||
|
||||
self._g, edge_feature_list = self.preprocess(self.load_data, is_dynamic=True)
|
||||
self._g.ndata["hyperedge_feature"] = tensor(
|
||||
range(1, len(edge_feature_list) + 1)
|
||||
)
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = value
|
||||
@@ -0,0 +1,94 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from warnings import warn
|
||||
|
||||
import requests
|
||||
|
||||
from easygraph.convert import dict_to_hypergraph
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"load_dynamic_hypergraph_dataset",
|
||||
]
|
||||
|
||||
dataset_index_url = "https://gitlab.com/easy-graph/easygraph-data/-/raw/main/dataset_index.json?inline=false"
|
||||
|
||||
|
||||
def request_json_from_url(url):
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
raise EasyGraphError("Connection Error!")
|
||||
|
||||
if r.ok:
|
||||
return r.json()
|
||||
else:
|
||||
raise EasyGraphError(f"Error: HTTP response {r.status_code}")
|
||||
|
||||
|
||||
def _request_from_eg_data(dataset=None, cache=True):
|
||||
"""Request a dataset from eg-data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset : str, optional
|
||||
Dataset name. Valid options are the top-level tags of the
|
||||
index.json file in the xgi-data repository. If None, prints
|
||||
the list of available datasets.
|
||||
cache : bool, optional
|
||||
Whether or not to cache the output
|
||||
|
||||
Returns
|
||||
-------
|
||||
Data
|
||||
The requested data loaded from a json file.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If the HTTP request is not successful or the dataset does not exist.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
index_data = request_json_from_url(dataset_index_url)
|
||||
|
||||
key = dataset.lower()
|
||||
if key not in index_data:
|
||||
print("Valid dataset names:")
|
||||
print(*index_data, sep="\n")
|
||||
raise EasyGraphError("Must choose a valid dataset name!")
|
||||
|
||||
return request_json_from_url(index_data[key]["url"])
|
||||
|
||||
|
||||
def load_dynamic_hypergraph_dataset(
|
||||
dataset=None,
|
||||
local_read=False,
|
||||
path="",
|
||||
max_order=None,
|
||||
):
|
||||
index_datasets = request_json_from_url(dataset_index_url)
|
||||
if dataset is None:
|
||||
print("Please refer to available list")
|
||||
|
||||
print(*index_datasets, sep="\n")
|
||||
return
|
||||
|
||||
if local_read:
|
||||
cfp = os.path.join(path, dataset + ".json")
|
||||
if os.path.exists(cfp):
|
||||
data = json.load(open(cfp, "r"))
|
||||
return dict_to_hypergraph(data, max_order=max_order)
|
||||
else:
|
||||
warn(
|
||||
f"No local copy was found at {cfp}. The data is requested "
|
||||
"from the xgi-data repository instead. To download a local "
|
||||
"copy, use `download_xgi_data`."
|
||||
)
|
||||
data = _request_from_eg_data(dataset)
|
||||
return dict_to_hypergraph(
|
||||
data, max_order=max_order, is_dynamic=index_datasets[dataset]["is_dynamic"]
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Facebook Ego-Net Dataset
|
||||
|
||||
This dataset contains a subset of Facebook’s social network collected from
|
||||
survey participants in the SNAP EgoNet project. Nodes represent users, and
|
||||
edges indicate friendship links between them.
|
||||
|
||||
Each ego network is centered on a user and includes their friend connections
|
||||
and friend-to-friend connections. The `.circles` files contain labeled groups
|
||||
(i.e., communities) of friends identified by the ego user.
|
||||
|
||||
This version processes all ego-nets as a single undirected graph. Node features
|
||||
are not provided. Labels (circles) are optional and not included by default.
|
||||
|
||||
Statistics (based on merged graph):
|
||||
- Nodes: ~4,000+
|
||||
- Edges: ~88,000+
|
||||
- Features: None
|
||||
- Classes: None
|
||||
|
||||
Reference:
|
||||
J. McAuley and J. Leskovec, “Learning to Discover Social Circles in Ego Networks,”
|
||||
in NIPS, 2012. [https://snap.stanford.edu/data/egonets-Facebook.html]
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import download
|
||||
from .utils import extract_archive
|
||||
|
||||
|
||||
class FacebookEgoNetDataset(EasyGraphBuiltinDataset):
|
||||
r"""Facebook Ego-Net social network dataset.
|
||||
|
||||
Each node is a user, and edges represent friendship. The dataset
|
||||
includes 10 ego networks centered on different users.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str, optional
|
||||
Directory to store the raw downloaded files. Default: None
|
||||
force_reload : bool, optional
|
||||
Whether to re-download and process the dataset. Default: False
|
||||
verbose : bool, optional
|
||||
Whether to print detailed processing logs. Default: True
|
||||
transform : callable, optional
|
||||
Optional transform to apply on the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.datasets import FacebookEgoNetDataset
|
||||
>>> dataset = FacebookEgoNetDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> print("Nodes:", g.number_of_nodes())
|
||||
>>> print("Edges:", g.number_of_edges())
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
name = "facebook"
|
||||
url = "https://snap.stanford.edu/data/facebook.tar.gz"
|
||||
super(FacebookEgoNetDataset, self).__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
parent_dir = os.path.join(self.raw_path, "facebook")
|
||||
g = eg.Graph()
|
||||
|
||||
# Iterate over all .edges files in the subdirectory
|
||||
for filename in os.listdir(parent_dir):
|
||||
if filename.endswith(".edges"):
|
||||
edge_file = os.path.join(parent_dir, filename)
|
||||
|
||||
with open(edge_file, "r") as f:
|
||||
for line in f:
|
||||
u, v = map(int, line.strip().split())
|
||||
g.add_edge(u, v)
|
||||
|
||||
self._g = g
|
||||
self._num_nodes = g.number_of_nodes()
|
||||
self._num_edges = g.number_of_edges()
|
||||
|
||||
if self.verbose:
|
||||
print("Finished loading Facebook Ego-Net dataset.")
|
||||
print(f" NumNodes: {self._num_nodes}")
|
||||
print(f" NumEdges: {self._num_edges}")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "FacebookEgoNetDataset only contains one merged graph"
|
||||
return self._g if self._transform is None else self._transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
def download(self):
|
||||
r"""Automatically download data and extract it."""
|
||||
if self.url is not None:
|
||||
archive_path = os.path.join(self.raw_dir, self.name + ".tar.gz")
|
||||
download(self.url, path=archive_path)
|
||||
extract_archive(archive_path, self.raw_path)
|
||||
@@ -0,0 +1,129 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import data_type_dict
|
||||
from .utils import tensor
|
||||
|
||||
|
||||
class FlickrDataset(EasyGraphBuiltinDataset):
|
||||
r"""Flickr dataset for node classification.
|
||||
|
||||
Nodes are images and edges represent social tags co-occurrence.
|
||||
Node features are precomputed image embeddings. Labels indicate image categories.
|
||||
|
||||
Statistics:
|
||||
- Nodes: 89,250
|
||||
- Edges: 899,756
|
||||
- Classes: 7
|
||||
- Feature dim: 500
|
||||
|
||||
Source: GraphSAINT (https://arxiv.org/abs/1907.04931)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str, optional
|
||||
Custom directory to download the dataset. Default: None (uses standard cache dir).
|
||||
force_reload : bool, optional
|
||||
Whether to re-download and reprocess. Default: False.
|
||||
verbose : bool, optional
|
||||
Whether to print loading progress. Default: False.
|
||||
transform : callable, optional
|
||||
A transform applied to the graph on access.
|
||||
reorder : bool, optional
|
||||
Whether to apply graph reordering for locality (requires torch). Default: False.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.datasets import FlickrDataset
|
||||
>>> ds = FlickrDataset(verbose=True)
|
||||
>>> g = ds[0]
|
||||
>>> print(g.number_of_nodes(), g.number_of_edges(), ds.num_classes)
|
||||
>>> print(g.nodes[0]['feat'].shape, g.nodes[0]['label'])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=False,
|
||||
transform=None,
|
||||
reorder=False,
|
||||
):
|
||||
name = "flickr"
|
||||
url = self._get_dgl_url("dataset/flickr.zip")
|
||||
self._reorder = reorder
|
||||
super(FlickrDataset, self).__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
# Load adjacency
|
||||
coo = sp.load_npz(os.path.join(self.raw_path, "adj_full.npz"))
|
||||
g = eg.Graph()
|
||||
g.add_edges_from(list(zip(*coo.nonzero())))
|
||||
|
||||
# Load features
|
||||
feats = np.load(os.path.join(self.raw_path, "feats.npy"))
|
||||
# Load labels
|
||||
with open(os.path.join(self.raw_path, "class_map.json")) as f:
|
||||
class_map = json.load(f)
|
||||
labels = np.array([class_map[str(i)] for i in range(feats.shape[0])])
|
||||
|
||||
# Load train/val/test splits
|
||||
with open(os.path.join(self.raw_path, "role.json")) as f:
|
||||
role = json.load(f)
|
||||
train_mask = np.zeros(feats.shape[0], dtype=bool)
|
||||
train_mask[role["tr"]] = True
|
||||
val_mask = np.zeros(feats.shape[0], dtype=bool)
|
||||
val_mask[role["va"]] = True
|
||||
test_mask = np.zeros(feats.shape[0], dtype=bool)
|
||||
test_mask[role["te"]] = True
|
||||
|
||||
# Attach node data
|
||||
for i in range(feats.shape[0]):
|
||||
g.add_node(i, feat=feats[i].astype(np.float32), label=int(labels[i]))
|
||||
g.graph["train_mask"] = train_mask
|
||||
g.graph["val_mask"] = val_mask
|
||||
g.graph["test_mask"] = test_mask
|
||||
|
||||
self._g = g
|
||||
self._num_classes = int(labels.max() + 1)
|
||||
if self.verbose:
|
||||
print("Loaded Flickr dataset")
|
||||
print(
|
||||
f" Nodes: {g.number_of_nodes()}, Edges: {g.number_of_edges()}, Features: {feats.shape[1]}, Classes: {self._num_classes}"
|
||||
)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "FlickrDataset contains only one graph"
|
||||
g = self._g
|
||||
# transfer mask info
|
||||
g.graph["train_mask"] = g.graph.pop("train_mask")
|
||||
g.graph["val_mask"] = g.graph.pop("val_mask")
|
||||
g.graph["test_mask"] = g.graph.pop("test_mask")
|
||||
return self._transform(g) if self._transform else g
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
return self._num_classes
|
||||
|
||||
@staticmethod
|
||||
def _get_dgl_url(path):
|
||||
from .utils import _get_dgl_url
|
||||
|
||||
return _get_dgl_url(path)
|
||||
@@ -0,0 +1,210 @@
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
# import progressbar
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_graph_karateclub",
|
||||
"get_graph_blogcatalog",
|
||||
"get_graph_youtube",
|
||||
"get_graph_flickr",
|
||||
]
|
||||
|
||||
|
||||
def get_graph_karateclub():
|
||||
"""Returns the undirected graph of Karate Club.
|
||||
|
||||
Returns
|
||||
-------
|
||||
get_graph_karateclub : easygraph.Graph
|
||||
The undirected graph instance of karate club from dataset:
|
||||
http://vlado.fmf.uni-lj.si/pub/networks/data/Ucinet/UciData.htm
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] http://vlado.fmf.uni-lj.si/pub/networks/data/Ucinet/UciData.htm
|
||||
|
||||
"""
|
||||
all_members = set(range(34))
|
||||
club1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21}
|
||||
# club2 = all_members - club1
|
||||
|
||||
G = eg.Graph(name="Zachary's Karate Club")
|
||||
for node in all_members:
|
||||
G.add_node(node + 1)
|
||||
|
||||
zacharydat = """\
|
||||
0 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 1 0 0
|
||||
1 0 1 1 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 1 0 0 0
|
||||
1 1 0 1 0 0 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0
|
||||
1 1 1 0 0 0 0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1
|
||||
0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
|
||||
1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1
|
||||
0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1
|
||||
1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1
|
||||
1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 1 1
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 1 0 0
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1
|
||||
0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1
|
||||
0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1
|
||||
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 1
|
||||
0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1
|
||||
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 0 1 1
|
||||
0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 1 0 1 0 1 1 0 0 0 0 0 1 1 1 0 1
|
||||
0 0 0 0 0 0 0 0 1 1 0 0 0 1 1 1 0 0 1 1 1 0 1 1 0 0 1 1 1 1 1 1 1 0"""
|
||||
|
||||
for row, line in enumerate(zacharydat.split("\n")):
|
||||
thisrow = [int(b) for b in line.split()]
|
||||
for col, entry in enumerate(thisrow):
|
||||
if entry == 1:
|
||||
G.add_edge(row + 1, col + 1)
|
||||
|
||||
# Add the name of each member's club as a node attribute.
|
||||
for v in G:
|
||||
G.nodes[v]["club"] = "Mr. Hi" if v in club1 else "Officer"
|
||||
return G
|
||||
|
||||
|
||||
def get_graph_blogcatalog():
|
||||
"""Returns the undirected graph of blogcatalog.
|
||||
|
||||
Returns
|
||||
-------
|
||||
get_graph_blogcatalog : easygraph.Graph
|
||||
The undirected graph instance of blogcatalog from dataset:
|
||||
https://github.com/phanein/deepwalk/blob/master/example_graphs/blogcatalog.mat
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://github.com/phanein/deepwalk/blob/master/example_graphs/blogcatalog.mat
|
||||
|
||||
"""
|
||||
from scipy.io import loadmat
|
||||
|
||||
def sparse2graph(x):
|
||||
from collections import defaultdict
|
||||
|
||||
G = defaultdict(lambda: set())
|
||||
cx = x.tocoo()
|
||||
for i, j, v in zip(cx.row, cx.col, cx.data):
|
||||
G[i].add(j)
|
||||
return {str(k): [str(x) for x in v] for k, v in G.items()}
|
||||
|
||||
mat = loadmat("./samples/blogcatalog.mat")
|
||||
A = mat["network"]
|
||||
data = sparse2graph(A)
|
||||
|
||||
G = eg.Graph()
|
||||
for u in data:
|
||||
for v in data[u]:
|
||||
G.add_edge(u, v)
|
||||
|
||||
return G
|
||||
|
||||
|
||||
def get_graph_youtube():
|
||||
"""Returns the undirected graph of Youtube dataset.
|
||||
|
||||
Returns
|
||||
-------
|
||||
get_graph_youtube : easygraph.Graph
|
||||
The undirected graph instance of Youtube from dataset:
|
||||
http://socialnetworks.mpi-sws.mpg.de/data/youtube-links.txt.gz
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] http://socialnetworks.mpi-sws.mpg.de/data/youtube-links.txt.gz
|
||||
|
||||
"""
|
||||
import gzip
|
||||
|
||||
from urllib import request
|
||||
|
||||
url = "http://socialnetworks.mpi-sws.mpg.de/data/youtube-links.txt.gz"
|
||||
zipped_data_path = "./samples/youtube-links.txt.gz"
|
||||
unzipped_data_path = "./samples/youtube-links.txt"
|
||||
|
||||
# Download .gz file
|
||||
print("Downloading Youtube dataset...")
|
||||
request.urlretrieve(url, zipped_data_path, _show_progress)
|
||||
|
||||
# Unzip
|
||||
unzipped_data = gzip.GzipFile(zipped_data_path)
|
||||
open(unzipped_data_path, "wb+").write(unzipped_data.read())
|
||||
unzipped_data.close()
|
||||
|
||||
# Returns graph
|
||||
G = eg.Graph()
|
||||
G.add_edges_from_file(file=unzipped_data_path)
|
||||
return G
|
||||
|
||||
|
||||
def get_graph_flickr():
|
||||
"""Returns the undirected graph of Flickr dataset.
|
||||
|
||||
Returns
|
||||
-------
|
||||
get_graph_flickr : easygraph.Graph
|
||||
The undirected graph instance of Flickr from dataset:
|
||||
http://socialnetworks.mpi-sws.mpg.de/data/flickr-links.txt.gz
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] http://socialnetworks.mpi-sws.mpg.de/data/flickr-links.txt.gz
|
||||
|
||||
"""
|
||||
import gzip
|
||||
|
||||
from urllib import request
|
||||
|
||||
url = "http://socialnetworks.mpi-sws.mpg.de/data/flickr-links.txt.gz"
|
||||
zipped_data_path = "./samples/flickr-links.txt.gz"
|
||||
unzipped_data_path = "./samples/flickr-links.txt"
|
||||
|
||||
# Download .gz file
|
||||
print("Downloading Flickr dataset...")
|
||||
request.urlretrieve(url, zipped_data_path, _show_progress)
|
||||
|
||||
# Unzip
|
||||
unzipped_data = gzip.GzipFile(zipped_data_path)
|
||||
open(unzipped_data_path, "wb+").write(unzipped_data.read())
|
||||
unzipped_data.close()
|
||||
|
||||
# Returns graph
|
||||
G = eg.Graph()
|
||||
G.add_edges_from_file(file=unzipped_data_path)
|
||||
return G
|
||||
|
||||
|
||||
_pbar = None
|
||||
|
||||
|
||||
def _show_progress(block_num, block_size, total_size):
|
||||
global _pbar
|
||||
if _pbar is None:
|
||||
_pbar = progressbar.ProgressBar(maxval=total_size)
|
||||
_pbar.start()
|
||||
|
||||
downloaded = block_num * block_size
|
||||
if downloaded < total_size:
|
||||
_pbar.update(downloaded)
|
||||
else:
|
||||
_pbar.finish()
|
||||
_pbar = None
|
||||
@@ -0,0 +1,125 @@
|
||||
"""GitHub Users Social Network Dataset (musae_git)
|
||||
|
||||
This dataset represents a directed social network of GitHub users collected in 2019.
|
||||
Nodes represent GitHub developers, and a directed edge from user A to user B indicates that A follows B.
|
||||
|
||||
Each node also includes:
|
||||
- Features: User profile and activity-based features.
|
||||
- Labels: Developer's project area (e.g., machine learning, web dev, etc.)
|
||||
|
||||
Statistics:
|
||||
- Nodes: 37,700
|
||||
- Edges: 289,003
|
||||
- Feature dim: 5,575
|
||||
- Classes: 2
|
||||
|
||||
Reference:
|
||||
J. Leskovec et al. "SNAP Datasets: Stanford Large Network Dataset Collection",
|
||||
https://snap.stanford.edu/data/github-social.html
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import download
|
||||
from .utils import extract_archive
|
||||
|
||||
|
||||
class GitHubUsersDataset(EasyGraphBuiltinDataset):
|
||||
r"""GitHub developers social graph (musae_git).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str, optional
|
||||
Directory to store raw data. Default: None
|
||||
force_reload : bool, optional
|
||||
Force re-download and processing. Default: False
|
||||
verbose : bool, optional
|
||||
Print processing information. Default: True
|
||||
transform : callable, optional
|
||||
Transform to apply to the graph on load.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.datasets import GitHubUsersDataset
|
||||
>>> dataset = GitHubUsersDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> print("Nodes:", g.number_of_nodes())
|
||||
>>> print("Edges:", g.number_of_edges())
|
||||
>>> print("Feature shape:", g.nodes[0]['feat'].shape)
|
||||
>>> print("Label:", g.nodes[0]['label'])
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
name = "musae_git"
|
||||
url = "https://snap.stanford.edu/data/git_web_ml.zip"
|
||||
super(GitHubUsersDataset, self).__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def download(self):
|
||||
archive = os.path.join(self.raw_dir, self.name + ".zip")
|
||||
download(self.url, path=archive)
|
||||
extract_archive(archive, self.raw_path)
|
||||
|
||||
def process(self):
|
||||
g = eg.DiGraph()
|
||||
base_path = os.path.join(self.raw_path, "git_web_ml")
|
||||
|
||||
# Load node features
|
||||
with open(os.path.join(base_path, "musae_git_features.json"), "r") as f:
|
||||
features = json.load(f)
|
||||
|
||||
# Load labels
|
||||
labels = {}
|
||||
with open(os.path.join(base_path, "musae_git_target.csv"), "r") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
node_id = int(row["id"])
|
||||
labels[node_id] = int(row["ml_target"])
|
||||
|
||||
# Load edges
|
||||
with open(os.path.join(base_path, "musae_git_edges.csv"), "r") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
u, v = int(row["id_1"]), int(row["id_2"])
|
||||
g.add_edge(u, v)
|
||||
|
||||
# Add node attributes
|
||||
for node_id in g.nodes:
|
||||
feat = np.array(features[str(node_id)], dtype=np.float32)
|
||||
label = labels.get(node_id, -1)
|
||||
g.add_node(node_id, feat=feat, label=label)
|
||||
|
||||
self._g = g
|
||||
self._num_classes = len(set(labels.values()))
|
||||
|
||||
if self.verbose:
|
||||
print("Finished loading GitHub Users dataset.")
|
||||
print(f" NumNodes: {g.number_of_nodes()}")
|
||||
print(f" NumEdges: {g.number_of_edges()}")
|
||||
print(f" Feature dim: {feat.shape[0]}")
|
||||
print(f" NumClasses: {self._num_classes}")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "GitHubUsersDataset only contains one graph"
|
||||
return self._g if self._transform is None else self._transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
return self._num_classes
|
||||
@@ -0,0 +1,216 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import _get_dgl_url
|
||||
from .utils import _set_labels
|
||||
from .utils import data_type_dict
|
||||
from .utils import tensor
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AmazonCoBuyComputerDataset",
|
||||
]
|
||||
|
||||
|
||||
class GNNBenchmarkDataset(EasyGraphBuiltinDataset):
|
||||
r"""Base Class for GNN Benchmark dataset
|
||||
|
||||
Reference: https://github.com/shchur/gnn-benchmark#datasets
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, name, raw_dir=None, force_reload=False, verbose=True, transform=None
|
||||
):
|
||||
_url = _get_dgl_url("dataset/" + name + ".zip")
|
||||
super(GNNBenchmarkDataset, self).__init__(
|
||||
name=name,
|
||||
url=_url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
npz_path = os.path.join(self.raw_path, self.name + ".npz")
|
||||
g = self._load_npz(npz_path)
|
||||
# g = transforms.reorder_graph(
|
||||
# g, node_permute_algo='rcmk', edge_permute_algo='dst', store_ids=False)
|
||||
self._graph = g
|
||||
self._data = [g]
|
||||
self._print_info()
|
||||
|
||||
def has_cache(self):
|
||||
graph_path = os.path.join(self.save_path, "dgl_graph_v1.bin")
|
||||
if os.path.exists(graph_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
# def save(self):
|
||||
# graph_path = os.path.join(self.save_path, 'dgl_graph_v1.bin')
|
||||
# save_graphs(graph_path, self._graph)
|
||||
#
|
||||
# def load(self):
|
||||
# graph_path = os.path.join(self.save_path, 'dgl_graph_v1.bin')
|
||||
# graphs, _ = load_graphs(graph_path)
|
||||
# self._graph = graphs[0]
|
||||
# self._data = [graphs[0]]
|
||||
# self._print_info()
|
||||
|
||||
def _print_info(self):
|
||||
if self.verbose:
|
||||
print(" NumNodes: {}".format(self._graph.number_of_nodes()))
|
||||
print(" NumEdges: {}".format(2 * self._graph.number_of_edges()))
|
||||
print(" NumFeats: {}".format(self._graph.ndata["feat"].shape[-1]))
|
||||
print(" NumbClasses: {}".format(self.num_classes))
|
||||
|
||||
def _load_npz(self, file_name):
|
||||
with np.load(file_name, allow_pickle=True) as loader:
|
||||
loader = dict(loader)
|
||||
num_nodes = loader["adj_shape"][0]
|
||||
adj_matrix = sp.csr_matrix(
|
||||
(loader["adj_data"], loader["adj_indices"], loader["adj_indptr"]),
|
||||
shape=loader["adj_shape"],
|
||||
).tocoo()
|
||||
|
||||
if "attr_data" in loader:
|
||||
# Attributes are stored as a sparse CSR matrix
|
||||
attr_matrix = sp.csr_matrix(
|
||||
(
|
||||
loader["attr_data"],
|
||||
loader["attr_indices"],
|
||||
loader["attr_indptr"],
|
||||
),
|
||||
shape=loader["attr_shape"],
|
||||
).todense()
|
||||
elif "attr_matrix" in loader:
|
||||
# Attributes are stored as a (dense) np.ndarray
|
||||
attr_matrix = loader["attr_matrix"]
|
||||
else:
|
||||
attr_matrix = None
|
||||
|
||||
if "labels_data" in loader:
|
||||
# Labels are stored as a CSR matrix
|
||||
labels = sp.csr_matrix(
|
||||
(
|
||||
loader["labels_data"],
|
||||
loader["labels_indices"],
|
||||
loader["labels_indptr"],
|
||||
),
|
||||
shape=loader["labels_shape"],
|
||||
).todense()
|
||||
elif "labels" in loader:
|
||||
# Labels are stored as a numpy array
|
||||
labels = loader["labels"]
|
||||
else:
|
||||
labels = None
|
||||
if hasattr(adj_matrix, "format"):
|
||||
print("can be generate eg!")
|
||||
g = Graph(incoming_graph_data=adj_matrix)
|
||||
# g = transforms.to_bidirected(g)
|
||||
g = _set_labels(g, labels)
|
||||
g.ndata["feat"] = tensor(attr_matrix, data_type_dict()["float32"])
|
||||
g.ndata["label"] = tensor(labels, data_type_dict()["int64"])
|
||||
return g
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
"""Number of classes."""
|
||||
raise NotImplementedError
|
||||
|
||||
def __getitem__(self, idx):
|
||||
r"""Get graph by index
|
||||
|
||||
Parameters
|
||||
----------
|
||||
idx : int
|
||||
Item index
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`dgl.DGLGraph`
|
||||
|
||||
The graph contains:
|
||||
|
||||
- ``ndata['feat']``: node features
|
||||
- ``ndata['label']``: node labels
|
||||
"""
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
if self._transform is None:
|
||||
return self._graph
|
||||
else:
|
||||
return self._transform(self._graph)
|
||||
|
||||
def __len__(self):
|
||||
r"""Number of graphs in the dataset"""
|
||||
return 1
|
||||
|
||||
|
||||
class AmazonCoBuyComputerDataset(GNNBenchmarkDataset):
|
||||
r"""'Computer' part of the AmazonCoBuy dataset for node classification task.
|
||||
|
||||
Amazon Computers and Amazon Photo are segments of the Amazon co-purchase graph [McAuley et al., 2015],
|
||||
where nodes represent goods, edges indicate that two goods are frequently bought together, node
|
||||
features are bag-of-words encoded product reviews, and class labels are given by the product category.
|
||||
|
||||
Reference: `<https://github.com/shchur/gnn-benchmark#datasets>`_
|
||||
|
||||
Statistics:
|
||||
|
||||
- Nodes: 13,752
|
||||
- Edges: 491,722 (note that the original dataset has 245,778 edges but DGL adds
|
||||
the reverse edges and remove the duplicates, hence with a different number)
|
||||
- Number of classes: 10
|
||||
- Node feature size: 767
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.dgl/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
num_classes : int
|
||||
Number of classes for each node.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> data = AmazonCoBuyComputerDataset()
|
||||
>>> g = data[0]
|
||||
>>> num_class = data.num_classes
|
||||
>>> feat = g.ndata['feat'] # get node feature
|
||||
>>> label = g.ndata['label'] # get node labels
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
super(AmazonCoBuyComputerDataset, self).__init__(
|
||||
name="amazon_co_buy_computer",
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
"""Number of classes.
|
||||
|
||||
Return
|
||||
-------
|
||||
int
|
||||
"""
|
||||
return 10
|
||||
@@ -0,0 +1,318 @@
|
||||
"""Basic EasyGraph Dataset"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import abc
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from ..utils import retry_method_with_fix
|
||||
from .utils import download
|
||||
from .utils import extract_archive
|
||||
from .utils import get_download_dir
|
||||
from .utils import makedirs
|
||||
|
||||
|
||||
class EasyGraphDataset(object):
|
||||
r"""The basic EasyGraph dataset for creating graph datasets.
|
||||
This class defines a basic template class for EasyGraph Dataset.
|
||||
The following steps will be executed automatically:
|
||||
|
||||
1. Check whether there is a dataset cache on disk
|
||||
(already processed and stored on the disk) by
|
||||
invoking ``has_cache()``. If true, goto 5.
|
||||
2. Call ``download()`` to download the data if ``url`` is not None.
|
||||
3. Call ``process()`` to process the data.
|
||||
4. Call ``save()`` to save the processed dataset on disk and goto 6.
|
||||
5. Call ``load()`` to load the processed dataset from disk.
|
||||
6. Done.
|
||||
|
||||
Users can overwrite these functions with their
|
||||
own data processing logic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name of the dataset
|
||||
url : str
|
||||
Url to download the raw dataset. Default: None
|
||||
raw_dir : str
|
||||
Specifying the directory that will store the
|
||||
downloaded data or the directory that
|
||||
already stores the input data.
|
||||
Default: ~/.EasyGraphData/
|
||||
save_dir : str
|
||||
Directory to save the processed dataset.
|
||||
Default: same as raw_dir
|
||||
hash_key : tuple
|
||||
A tuple of values as the input for the hash function.
|
||||
Users can distinguish instances (and their caches on the disk)
|
||||
from the same dataset class by comparing the hash values.
|
||||
Default: (), the corresponding hash value is ``'f9065fa7'``.
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
url=None,
|
||||
raw_dir=None,
|
||||
save_dir=None,
|
||||
hash_key=(),
|
||||
force_reload=False,
|
||||
verbose=False,
|
||||
transform=None,
|
||||
):
|
||||
self._name = name
|
||||
self._url = url
|
||||
self._force_reload = force_reload
|
||||
self._verbose = verbose
|
||||
self._hash_key = hash_key
|
||||
self._hash = self._get_hash()
|
||||
self._transform = transform
|
||||
|
||||
# if no dir is provided, the default EasyGraph download dir is used.
|
||||
if raw_dir is None:
|
||||
self._raw_dir = get_download_dir()
|
||||
else:
|
||||
self._raw_dir = raw_dir
|
||||
|
||||
if save_dir is None:
|
||||
self._save_dir = self._raw_dir
|
||||
else:
|
||||
self._save_dir = save_dir
|
||||
self._load()
|
||||
|
||||
def download(self):
|
||||
r"""Overwrite to realize your own logic of downloading data.
|
||||
|
||||
It is recommended to download the to the :obj:`self.raw_dir`
|
||||
folder. Can be ignored if the dataset is
|
||||
already in :obj:`self.raw_dir`.
|
||||
"""
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
r"""Overwrite to realize your own logic of
|
||||
saving the processed dataset into files.
|
||||
|
||||
It is recommended to use ``dgl.data.utils.save_graphs``
|
||||
to save dgl graph into files and use
|
||||
``dgl.data.utils.save_info`` to save extra
|
||||
information into files.
|
||||
"""
|
||||
pass
|
||||
|
||||
def load(self):
|
||||
r"""Overwrite to realize your own logic of
|
||||
loading the saved dataset from files.
|
||||
|
||||
It is recommended to use ``dgl.data.utils.load_graphs``
|
||||
to load dgl graph from files and use
|
||||
``dgl.data.utils.load_info`` to load extra information
|
||||
into python dict object.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def process(self):
|
||||
r"""Overwrite to realize your own logic of processing the input data."""
|
||||
pass
|
||||
|
||||
def has_cache(self):
|
||||
r"""Overwrite to realize your own logic of
|
||||
deciding whether there exists a cached dataset.
|
||||
|
||||
By default False.
|
||||
"""
|
||||
return False
|
||||
|
||||
@retry_method_with_fix(download)
|
||||
def _download(self):
|
||||
"""Download dataset by calling ``self.download()``
|
||||
if the dataset does not exists under ``self.raw_path``.
|
||||
|
||||
By default ``self.raw_path = os.path.join(self.raw_dir, self.name)``
|
||||
One can overwrite ``raw_path()`` function to change the path.
|
||||
"""
|
||||
|
||||
if os.path.exists(self.raw_path): # pragma: no cover
|
||||
return
|
||||
|
||||
makedirs(self.raw_dir)
|
||||
self.download()
|
||||
|
||||
def _load(self):
|
||||
"""Entry point from __init__ to load the dataset.
|
||||
|
||||
If cache exists:
|
||||
|
||||
- Load the dataset from saved dgl graph and information files.
|
||||
- If loading process fails, re-download and process the dataset.
|
||||
|
||||
else:
|
||||
|
||||
- Download the dataset if needed.
|
||||
- Process the dataset and build the dgl graph.
|
||||
- Save the processed dataset into files.
|
||||
"""
|
||||
|
||||
load_flag = not self._force_reload and self.has_cache()
|
||||
if load_flag:
|
||||
try:
|
||||
self.load()
|
||||
self.process()
|
||||
if self.verbose:
|
||||
print("Done loading data from cached files.")
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
load_flag = False
|
||||
if self.verbose:
|
||||
print(traceback.format_exc())
|
||||
print("Loading from cache failed, re-processing.")
|
||||
|
||||
if not load_flag:
|
||||
self._download()
|
||||
self.process()
|
||||
self.save()
|
||||
if self.verbose:
|
||||
print("Done saving data into cached files.")
|
||||
|
||||
def _get_hash(self):
|
||||
"""Compute the hash of the input tuple
|
||||
|
||||
Example
|
||||
-------
|
||||
Assume `self._hash_key = (10, False, True)`
|
||||
|
||||
>>> hash_value = self._get_hash()
|
||||
>>> hash_value
|
||||
'a770b222'
|
||||
"""
|
||||
hash_func = hashlib.sha1()
|
||||
hash_func.update(str(self._hash_key).encode("utf-8"))
|
||||
return hash_func.hexdigest()[:8]
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
r"""Get url to download the raw dataset."""
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
r"""Name of the dataset."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def raw_dir(self):
|
||||
r"""Raw file directory contains the input data folder."""
|
||||
return self._raw_dir
|
||||
|
||||
@property
|
||||
def raw_path(self):
|
||||
r"""Directory contains the input data files.
|
||||
By default raw_path = os.path.join(self.raw_dir, self.name)
|
||||
"""
|
||||
return os.path.join(self.raw_dir, self.name)
|
||||
|
||||
@property
|
||||
def save_dir(self):
|
||||
r"""Directory to save the processed dataset."""
|
||||
return self._save_dir
|
||||
|
||||
@property
|
||||
def save_path(self):
|
||||
r"""Path to save the processed dataset."""
|
||||
return os.path.join(self._save_dir)
|
||||
|
||||
@property
|
||||
def verbose(self):
|
||||
r"""Whether to print information."""
|
||||
return self._verbose
|
||||
|
||||
@property
|
||||
def hash(self):
|
||||
r"""Hash value for the dataset and the setting."""
|
||||
return self._hash
|
||||
|
||||
@abc.abstractmethod
|
||||
def __getitem__(self, idx):
|
||||
r"""Gets the data object at index."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def __len__(self):
|
||||
r"""The number of examples in the dataset."""
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return f'Dataset("{self.name}"' + f" save_path={self.save_path})"
|
||||
|
||||
|
||||
class EasyGraphBuiltinDataset(EasyGraphDataset):
|
||||
r"""The Basic EasyGraph Builtin Dataset.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name of the dataset.
|
||||
url : str
|
||||
Url to download the raw dataset.
|
||||
raw_dir : str
|
||||
Specifying the directory that will store the
|
||||
downloaded data or the directory that
|
||||
already stores the input data.
|
||||
Default: ~/.dgl/
|
||||
hash_key : tuple
|
||||
A tuple of values as the input for the hash function.
|
||||
Users can distinguish instances (and their caches on the disk)
|
||||
from the same dataset class by comparing the hash values.
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information. Default: False
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~dgl.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
url,
|
||||
raw_dir=None,
|
||||
hash_key=(),
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
transform=None,
|
||||
save_dir=None,
|
||||
):
|
||||
super(EasyGraphBuiltinDataset, self).__init__(
|
||||
name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
save_dir=save_dir,
|
||||
hash_key=hash_key,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def download(self):
|
||||
r"""Automatically download data and extract it."""
|
||||
if self.url is not None:
|
||||
zip_file_path = os.path.join(self.raw_dir, self.name + ".zip")
|
||||
download(self.url, path=zip_file_path)
|
||||
extract_archive(zip_file_path, self.raw_path)
|
||||
@@ -0,0 +1,216 @@
|
||||
import requests
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
def request_text_from_url(url):
|
||||
"""
|
||||
Requests text data from the specified URL.
|
||||
|
||||
Args:
|
||||
url (str): The URL from which to request the text data.
|
||||
|
||||
Returns:
|
||||
str: The text content of the response if the request is successful.
|
||||
|
||||
Raises:
|
||||
EasyGraphError: If a connection error occurs during the request or if the HTTP response status code
|
||||
indicates a failure.
|
||||
"""
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
raise EasyGraphError("Connection Error!")
|
||||
|
||||
if r.ok:
|
||||
return r.text
|
||||
else:
|
||||
raise EasyGraphError(f"Error: HTTP response {r.status_code}")
|
||||
|
||||
|
||||
class House_Committees:
|
||||
"""
|
||||
A class for loading and processing the House Committees hypergraph dataset.
|
||||
|
||||
This class fetches hyperedge, node label, node name, and label name data from predefined URLs,
|
||||
processes the data, and generates a hypergraph representation. It also provides access to various
|
||||
dataset attributes through properties and indexing.
|
||||
|
||||
Attributes:
|
||||
data_root (str): The root URL for the data. If `data_root` is provided during initialization,
|
||||
it is set to "https://"; otherwise, it is `None`.
|
||||
hyperedges_path (str): The URL of the file containing hyperedge information.
|
||||
node_labels_path (str): The URL of the file containing node label information.
|
||||
node_names_path (str): The URL of the file containing node name information.
|
||||
label_names_path (str): The URL of the file containing label name information.
|
||||
_hyperedges (list): A list of tuples representing hyperedges.
|
||||
_node_labels (list): A list of node labels.
|
||||
_label_names (list): A list of label names.
|
||||
_node_names (list): A list of node names.
|
||||
_content (dict): A dictionary containing dataset statistics and data, including the number of
|
||||
classes, vertices, edges, the edge list, and node labels.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root=None):
|
||||
"""
|
||||
Initializes a new instance of the `House_Committees` class.
|
||||
|
||||
Args:
|
||||
data_root (str, optional): The root URL for the data. If provided, it is set to "https://";
|
||||
otherwise, it is `None`. Defaults to `None`.
|
||||
"""
|
||||
self.data_root = "https://" if data_root is not None else data_root
|
||||
self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/hyperedges-house-committees.txt?inline=false"
|
||||
self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/node-labels-house-committees.txt?ref_type=heads&inline=false"
|
||||
self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false"
|
||||
self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/label-names-house-committees.txt?ref_type=heads&inline=false"
|
||||
self._hyperedges = []
|
||||
self._node_labels = []
|
||||
self._label_names = []
|
||||
self._node_names = []
|
||||
self.generate_hypergraph(
|
||||
hyperedges_path=self.hyperedges_path,
|
||||
node_labels_path=self.node_labels_path,
|
||||
node_names_path=self.node_names_path,
|
||||
label_names_path=self.label_names_path,
|
||||
)
|
||||
|
||||
self._content = {
|
||||
"num_classes": len(self._label_names),
|
||||
"num_vertices": len(self._node_labels),
|
||||
"num_edges": len(self._hyperedges),
|
||||
"edge_list": self._hyperedges,
|
||||
"labels": self._node_labels,
|
||||
}
|
||||
|
||||
def process_label_txt(self, data_str, delimiter="\n", transform_fun=str):
|
||||
"""
|
||||
Processes a string containing label data into a list of transformed values.
|
||||
|
||||
Args:
|
||||
data_str (str): The input string containing label data.
|
||||
delimiter (str, optional): The delimiter used to split the input string. Defaults to "\n".
|
||||
transform_fun (callable, optional): A function used to transform each label value.
|
||||
Defaults to the `str` function.
|
||||
|
||||
Returns:
|
||||
list: A list of transformed label values.
|
||||
"""
|
||||
data_str = data_str.strip()
|
||||
data_lst = data_str.split(delimiter)
|
||||
final_lst = []
|
||||
for data in data_lst:
|
||||
data = data.strip()
|
||||
data = transform_fun(data)
|
||||
final_lst.append(data)
|
||||
return final_lst
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
"""
|
||||
Retrieves a value from the `_content` dictionary using the specified key.
|
||||
|
||||
Args:
|
||||
key (str): The key used to access the `_content` dictionary.
|
||||
|
||||
Returns:
|
||||
Any: The value corresponding to the key in the `_content` dictionary.
|
||||
"""
|
||||
return self._content[key]
|
||||
|
||||
@property
|
||||
def node_labels(self):
|
||||
"""
|
||||
Gets the list of node labels.
|
||||
|
||||
Returns:
|
||||
list: A list of node labels.
|
||||
"""
|
||||
return self._node_labels
|
||||
|
||||
@property
|
||||
def node_names(self):
|
||||
"""
|
||||
Gets the list of node names.
|
||||
|
||||
Returns:
|
||||
list: A list of node names.
|
||||
"""
|
||||
return self._node_names
|
||||
|
||||
@property
|
||||
def label_names(self):
|
||||
"""
|
||||
Gets the list of label names.
|
||||
|
||||
Returns:
|
||||
list: A list of label names.
|
||||
"""
|
||||
return self._label_names
|
||||
|
||||
@property
|
||||
def hyperedges(self):
|
||||
"""
|
||||
Gets the list of hyperedges.
|
||||
|
||||
Returns:
|
||||
list: A list of tuples representing hyperedges.
|
||||
"""
|
||||
return self._hyperedges
|
||||
|
||||
def generate_hypergraph(
|
||||
self,
|
||||
hyperedges_path=None,
|
||||
node_labels_path=None,
|
||||
node_names_path=None,
|
||||
label_names_path=None,
|
||||
):
|
||||
"""
|
||||
Generates a hypergraph by fetching and processing data from the specified URLs.
|
||||
|
||||
Args:
|
||||
hyperedges_path (str, optional): The URL of the file containing hyperedge information.
|
||||
Defaults to `None`.
|
||||
node_labels_path (str, optional): The URL of the file containing node label information.
|
||||
Defaults to `None`.
|
||||
node_names_path (str, optional): The URL of the file containing node name information.
|
||||
Defaults to `None`.
|
||||
label_names_path (str, optional): The URL of the file containing label name information.
|
||||
Defaults to `None`.
|
||||
"""
|
||||
|
||||
def fun(data):
|
||||
"""
|
||||
Converts a string to an integer and subtracts 1.
|
||||
|
||||
Args:
|
||||
data (str): The input string to be converted.
|
||||
|
||||
Returns:
|
||||
int: The converted integer value minus 1.
|
||||
"""
|
||||
data = int(data) - 1
|
||||
return data
|
||||
|
||||
hyperedges_info = request_text_from_url(hyperedges_path)
|
||||
hyperedges_info = hyperedges_info.strip()
|
||||
hyperedges_lst = hyperedges_info.split("\n")
|
||||
for hyperedge in hyperedges_lst:
|
||||
hyperedge = hyperedge.strip()
|
||||
hyperedge = [int(i) - 1 for i in hyperedge.split(",")]
|
||||
self._hyperedges.append(tuple(hyperedge))
|
||||
# print(self.hyperedges)
|
||||
|
||||
node_labels_info = request_text_from_url(node_labels_path)
|
||||
|
||||
process_node_labels_info = self.process_label_txt(
|
||||
node_labels_info, transform_fun=fun
|
||||
)
|
||||
self._node_labels = process_node_labels_info
|
||||
# print("process_node_labels_info:", process_node_labels_info)
|
||||
node_names_info = request_text_from_url(node_names_path)
|
||||
process_node_names_info = self.process_label_txt(node_names_info)
|
||||
self._node_names = process_node_names_info
|
||||
# print("process_node_names_info:", process_node_names_info)
|
||||
label_names_info = request_text_from_url(label_names_path)
|
||||
process_label_names_info = self.process_label_txt(label_names_info)
|
||||
self._label_names = process_label_names_info
|
||||
@@ -0,0 +1,82 @@
|
||||
from typing import Optional
|
||||
|
||||
from easygraph.datapipe import load_from_pickle
|
||||
from easygraph.datapipe import to_long_tensor
|
||||
from easygraph.datapipe import to_tensor
|
||||
from easygraph.datasets.hypergraph.hypergraph_dataset_base import BaseData
|
||||
|
||||
|
||||
class YelpRestaurant(BaseData):
|
||||
r"""The Yelp-Restaurant dataset is a restaurant-review network dataset for node classification task.
|
||||
|
||||
More details see the DHG or `YOU ARE ALLSET: A MULTISET LEARNING FRAMEWORK FOR HYPERGRAPH NEURAL NETWORKS <https://openreview.net/pdf?id=hpBTIv2uy_E>`_ paper.
|
||||
|
||||
The content of the Yelp-Restaurant dataset includes the following:
|
||||
|
||||
- ``num_classes``: The number of classes: :math:`11`.
|
||||
- ``num_vertices``: The number of vertices: :math:`50,758`.
|
||||
- ``num_edges``: The number of edges: :math:`679,302`.
|
||||
- ``dim_features``: The dimension of features: :math:`1,862`.
|
||||
- ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(50,758 \times 1,862)`.
|
||||
- ``edge_list``: The edge list. ``List`` with length :math:`679,302`.
|
||||
- ``labels``: The label list. ``torch.LongTensor`` with size :math:`(50,758, )`.
|
||||
- ``state``: The state list. ``torch.LongTensor`` with size :math:`(50,758, )`.
|
||||
- ``city``: The city list. ``torch.LongTensor`` with size :math:`(50,758, )`.
|
||||
|
||||
Args:
|
||||
``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root: Optional[str] = None) -> None:
|
||||
super().__init__("yelp_restaurant", data_root)
|
||||
self._content = {
|
||||
"num_classes": 11,
|
||||
"num_vertices": 50758,
|
||||
"num_edges": 679302,
|
||||
"dim_features": 1862,
|
||||
"features": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "features.pkl",
|
||||
"md5": "cedc4443884477c2e626025411c44cd7",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [
|
||||
to_tensor,
|
||||
],
|
||||
},
|
||||
"edge_list": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "edge_list.pkl",
|
||||
"md5": "4b26eecaa22305dd10edcd6372eb49da",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
},
|
||||
"labels": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "labels.pkl",
|
||||
"md5": "1cdc1ed9fb1f57b2accaa42db214d4ef",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
"state": {
|
||||
"upon": [
|
||||
{"filename": "state.pkl", "md5": "eef3b835fad37409f29ad36539296b57"}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
"city": {
|
||||
"upon": [
|
||||
{"filename": "city.pkl", "md5": "8302b167262b23067698e865cacd0b17"}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
from .cat_edge_Cooking import *
|
||||
from .coauthorship import *
|
||||
from .cocitation import *
|
||||
from .contact_primary_school import *
|
||||
from .House_Committees import *
|
||||
from .mathoverflow_answers import *
|
||||
from .senate_committees import *
|
||||
from .trivago_clicks import *
|
||||
from .walmart_trips import *
|
||||
from .Yelp import *
|
||||
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_eg_cache_root():
|
||||
root = Path.home() / Path(".easygraph/")
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
return root
|
||||
|
||||
|
||||
CACHE_ROOT = get_eg_cache_root()
|
||||
DATASETS_ROOT = CACHE_ROOT / "datasets"
|
||||
|
||||
REMOTE_ROOT = "https://download.moon-lab.tech:28501/"
|
||||
REMOTE_DATASETS_ROOT = REMOTE_ROOT + "datasets/"
|
||||
@@ -0,0 +1,104 @@
|
||||
import requests
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
def request_text_from_url(url):
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
raise EasyGraphError("Connection Error!")
|
||||
|
||||
if r.ok:
|
||||
return r.text
|
||||
else:
|
||||
raise EasyGraphError(f"Error: HTTP response {r.status_code}")
|
||||
|
||||
|
||||
class cat_edge_Cooking:
|
||||
def __init__(self, data_root=None):
|
||||
self.data_root = "https://" if data_root is not None else data_root
|
||||
self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-cat-edge-cooking/-/raw/main/hyperedges.txt?inline=false"
|
||||
self.edge_labels_path = "https://gitlab.com/easy-graph/easygraph-data-cat-edge-cooking/-/raw/main/hyperedge-labels.txt?ref_type=heads&inline=false"
|
||||
self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-cat-edge-cooking/-/raw/main/main/node-labels.txt?ref_type=heads&inline=false"
|
||||
self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-cat-edge-cooking/-/raw/main/hyperedge-label-identities.txt?ref_type=heads&inline=false"
|
||||
# self.hyperedges_path = []
|
||||
# self.edge_labels_path = []
|
||||
# self.node_names_path = []
|
||||
# self.label_names_path = []
|
||||
self.generate_hypergraph(
|
||||
hyperedges_path=self.hyperedges_path,
|
||||
edge_labels_path=self.edge_labels_path,
|
||||
node_names_path=self.node_names_path,
|
||||
label_names_path=self.label_names_path,
|
||||
)
|
||||
self._content = {
|
||||
"num_classes": len(self._label_names),
|
||||
"num_vertices": len(self._node_labels),
|
||||
"num_edges": len(self._hyperedges),
|
||||
"edge_list": self._hyperedges,
|
||||
"labels": self._node_labels,
|
||||
}
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
return self._content[key]
|
||||
|
||||
def process_label_txt(self, data_str, delimiter="\n", transform_fun=str):
|
||||
data_str = data_str.strip()
|
||||
data_lst = data_str.split(delimiter)
|
||||
final_lst = []
|
||||
for data in data_lst:
|
||||
data = data.strip()
|
||||
data = transform_fun(data)
|
||||
final_lst.append(data)
|
||||
return final_lst
|
||||
|
||||
@property
|
||||
def edge_labels(self):
|
||||
return self._edge_labels
|
||||
|
||||
@property
|
||||
def node_names(self):
|
||||
return self._node_names
|
||||
|
||||
@property
|
||||
def label_names(self):
|
||||
return self._label_names
|
||||
|
||||
@property
|
||||
def hyperedges(self):
|
||||
return self._hyperedges
|
||||
|
||||
def generate_hypergraph(
|
||||
self,
|
||||
hyperedges_path=None,
|
||||
node_labels_path=None,
|
||||
node_names_path=None,
|
||||
label_names_path=None,
|
||||
):
|
||||
def fun(data):
|
||||
data = int(data) - 1
|
||||
return data
|
||||
|
||||
hyperedges_info = request_text_from_url(hyperedges_path)
|
||||
hyperedges_info = hyperedges_info.strip()
|
||||
hyperedges_lst = hyperedges_info.split("\n")
|
||||
for hyperedge in hyperedges_lst:
|
||||
hyperedge = hyperedge.strip()
|
||||
hyperedge = [int(i) - 1 for i in hyperedge.split(" ")]
|
||||
self._hyperedges.append(tuple(hyperedge))
|
||||
# print(self.hyperedges)
|
||||
|
||||
edge_labels_info = request_text_from_url(self.edge_labels_path)
|
||||
process_node_labels_info = self.process_label_txt(
|
||||
node_labels_info, transform_fun=fun
|
||||
)
|
||||
self._edge_labels = process_edge_labels_info()
|
||||
|
||||
node_names_info = request_text_from_url(node_names_path)
|
||||
process_node_names_info = self.process_label_txt(node_names_info)
|
||||
self._node_names = process_node_names_info
|
||||
|
||||
label_names_info = request_text_from_url(label_names_path)
|
||||
process_label_names_info = self.process_label_txt(label_names_info)
|
||||
self._label_names = process_label_names_info
|
||||
@@ -0,0 +1,192 @@
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
|
||||
from easygraph.datapipe import load_from_pickle
|
||||
from easygraph.datapipe import norm_ft
|
||||
from easygraph.datapipe import to_bool_tensor
|
||||
from easygraph.datapipe import to_long_tensor
|
||||
from easygraph.datapipe import to_tensor
|
||||
from easygraph.datasets.hypergraph.hypergraph_dataset_base import BaseData
|
||||
|
||||
|
||||
__all__ = ["CoauthorshipCora", "CoauthorshipDBLP"]
|
||||
|
||||
|
||||
class CoauthorshipCora(BaseData):
|
||||
r"""The Co-authorship Cora dataset is a citation network dataset for vertex classification task.
|
||||
More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper.
|
||||
|
||||
The content of the Co-authorship Cora dataset includes the following:
|
||||
|
||||
- ``num_classes``: The number of classes: :math:`7`.
|
||||
- ``num_vertices``: The number of vertices: :math:`2,708`.
|
||||
- ``num_edges``: The number of edges: :math:`1,072`.
|
||||
- ``dim_features``: The dimension of features: :math:`1,433`.
|
||||
- ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(2,708 \times 1,433)`.
|
||||
- ``edge_list``: The edge list. ``List`` with length :math:`1,072`.
|
||||
- ``labels``: The label list. ``torch.LongTensor`` with size :math:`(2,708, )`.
|
||||
- ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(2,708, )`.
|
||||
- ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(2,708, )`.
|
||||
- ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(2,708, )`.
|
||||
|
||||
Args:
|
||||
``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root: Optional[str] = None) -> None:
|
||||
super().__init__("coauthorship_cora", data_root)
|
||||
self._content = {
|
||||
"num_classes": 7,
|
||||
"num_vertices": 2708,
|
||||
"num_edges": 1072,
|
||||
"dim_features": 1433,
|
||||
"features": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "features.pkl",
|
||||
"md5": "14257c0e24b4eb741b469a351e524785",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_tensor, partial(norm_ft, ord=1)],
|
||||
},
|
||||
"edge_list": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "edge_list.pkl",
|
||||
"md5": "a17ff337f1b9099f5a9d4d670674e146",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
},
|
||||
"labels": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "labels.pkl",
|
||||
"md5": "c8d11c452e0be69f79a47dd839279117",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
"train_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "train_mask.pkl",
|
||||
"md5": "111db6c6f986be2908378df7bdca7a9b",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"val_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "val_mask.pkl",
|
||||
"md5": "ffab1055193ffb2fe74822bb575d332a",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"test_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "test_mask.pkl",
|
||||
"md5": "ffab1055193ffb2fe74822bb575d332a",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class CoauthorshipDBLP(BaseData):
|
||||
r"""The Co-authorship DBLP dataset is a citation network dataset for vertex classification task.
|
||||
More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper.
|
||||
|
||||
The content of the Co-authorship DBLP dataset includes the following:
|
||||
|
||||
- ``num_classes``: The number of classes: :math:`6`.
|
||||
- ``num_vertices``: The number of vertices: :math:`41,302`.
|
||||
- ``num_edges``: The number of edges: :math:`22,363`.
|
||||
- ``dim_features``: The dimension of features: :math:`1,425`.
|
||||
- ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(41,302 \times 1,425)`.
|
||||
- ``edge_list``: The edge list. ``List`` with length :math:`22,363`.
|
||||
- ``labels``: The label list. ``torch.LongTensor`` with size :math:`(41,302, )`.
|
||||
- ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(41,302, )`.
|
||||
- ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(41,302, )`.
|
||||
- ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(41,302, )`.
|
||||
|
||||
Args:
|
||||
``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root: Optional[str] = None) -> None:
|
||||
super().__init__("coauthorship_dblp", data_root)
|
||||
self._content = {
|
||||
"num_classes": 6,
|
||||
"num_vertices": 41302,
|
||||
"num_edges": 22363,
|
||||
"dim_features": 1425,
|
||||
"features": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "features.pkl",
|
||||
"md5": "b78fd31b2586d1e19a40b3f6cd9cc2e7",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_tensor, partial(norm_ft, ord=1)],
|
||||
},
|
||||
"edge_list": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "edge_list.pkl",
|
||||
"md5": "c6bf5f9f3b9683bcc9b7bcc9eb8707d8",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
},
|
||||
"labels": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "labels.pkl",
|
||||
"md5": "2e7a792ea018028d582af8f02f2058ca",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
"train_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "train_mask.pkl",
|
||||
"md5": "a842b795c7cac4c2f98a56cf599bc1de",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"val_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "val_mask.pkl",
|
||||
"md5": "2ec4b7df7c5e6b355067a22c391ad578",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"test_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "test_mask.pkl",
|
||||
"md5": "2ec4b7df7c5e6b355067a22c391ad578",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
|
||||
from easygraph.datapipe import load_from_pickle
|
||||
from easygraph.datapipe import norm_ft
|
||||
from easygraph.datapipe import to_bool_tensor
|
||||
from easygraph.datapipe import to_long_tensor
|
||||
from easygraph.datapipe import to_tensor
|
||||
from easygraph.datasets.hypergraph.hypergraph_dataset_base import BaseData
|
||||
|
||||
|
||||
class CocitationCora(BaseData):
|
||||
r"""The Co-citation Cora dataset is a citation network dataset for vertex classification task.
|
||||
More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper.
|
||||
|
||||
The content of the Co-citation Cora dataset includes the following:
|
||||
|
||||
- ``num_classes``: The number of classes: :math:`7`.
|
||||
- ``num_vertices``: The number of vertices: :math:`2,708`.
|
||||
- ``num_edges``: The number of edges: :math:`1,579`.
|
||||
- ``dim_features``: The dimension of features: :math:`1,433`.
|
||||
- ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(2,708 \times 1,433)`.
|
||||
- ``edge_list``: The edge list. ``List`` with length :math:`1,579`.
|
||||
- ``labels``: The label list. ``torch.LongTensor`` with size :math:`(2,708, )`.
|
||||
- ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(2,708, )`.
|
||||
- ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(2,708, )`.
|
||||
- ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(2,708, )`.
|
||||
|
||||
Args:
|
||||
``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root: Optional[str] = None) -> None:
|
||||
super().__init__("cocitation_cora", data_root)
|
||||
self._content = {
|
||||
"num_classes": 7,
|
||||
"num_vertices": 2708,
|
||||
"num_edges": 1579,
|
||||
"dim_features": 1433,
|
||||
"features": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "features.pkl",
|
||||
"md5": "14257c0e24b4eb741b469a351e524785",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_tensor, partial(norm_ft, ord=1)],
|
||||
},
|
||||
"edge_list": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "edge_list.pkl",
|
||||
"md5": "e43d1321880c8ecb2260d8fb7effd9ea",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
},
|
||||
"labels": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "labels.pkl",
|
||||
"md5": "c8d11c452e0be69f79a47dd839279117",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
"train_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "train_mask.pkl",
|
||||
"md5": "111db6c6f986be2908378df7bdca7a9b",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"val_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "val_mask.pkl",
|
||||
"md5": "ffab1055193ffb2fe74822bb575d332a",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"test_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "test_mask.pkl",
|
||||
"md5": "ffab1055193ffb2fe74822bb575d332a",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class CocitationCiteseer(BaseData):
|
||||
r"""The Co-citation Citeseer dataset is a citation network dataset for vertex classification task.
|
||||
More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper.
|
||||
|
||||
The content of the Co-citation Citaseer dataset includes the following:
|
||||
|
||||
- ``num_classes``: The number of classes: :math:`6`.
|
||||
- ``num_vertices``: The number of vertices: :math:`3,312`.
|
||||
- ``num_edges``: The number of edges: :math:`1,079`.
|
||||
- ``dim_features``: The dimension of features: :math:`3,703`.
|
||||
- ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(3,312 \times 3,703)`.
|
||||
- ``edge_list``: The edge list. ``List`` with length :math:`1,079`.
|
||||
- ``labels``: The label list. ``torch.LongTensor`` with size :math:`(3,312, )`.
|
||||
- ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(3,312, )`.
|
||||
- ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(3,312, )`.
|
||||
- ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(3,312, )`.
|
||||
|
||||
Args:
|
||||
``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root: Optional[str] = None) -> None:
|
||||
super().__init__("cocitation_citeseer", data_root)
|
||||
self._content = {
|
||||
"num_classes": 6,
|
||||
"num_vertices": 3312,
|
||||
"num_edges": 1079,
|
||||
"dim_features": 3703,
|
||||
"features": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "features.pkl",
|
||||
"md5": "1ee0dc89e0d5f5ac9187b55a407683e8",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_tensor, partial(norm_ft, ord=1)],
|
||||
},
|
||||
"edge_list": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "edge_list.pkl",
|
||||
"md5": "6687b2e96159c534a424253f536b49ae",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
},
|
||||
"labels": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "labels.pkl",
|
||||
"md5": "71069f78e83fa85dd6a4b9b6570447c2",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
"train_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "train_mask.pkl",
|
||||
"md5": "3b831318fc3d3e588bead5ba469fe38f",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"val_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "val_mask.pkl",
|
||||
"md5": "c22eb5b7493908042c7e039c8bb5a82e",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"test_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "test_mask.pkl",
|
||||
"md5": "c22eb5b7493908042c7e039c8bb5a82e",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class CocitationPubmed(BaseData):
|
||||
r"""The Co-citation PubMed dataset is a citation network dataset for vertex classification task.
|
||||
More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper.
|
||||
|
||||
The content of the Co-citation PubMed dataset includes the following:
|
||||
|
||||
- ``num_classes``: The number of classes: :math:`3`.
|
||||
- ``num_vertices``: The number of vertices: :math:`19,717`.
|
||||
- ``num_edges``: The number of edges: :math:`7,963`.
|
||||
- ``dim_features``: The dimension of features: :math:`500`.
|
||||
- ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(19,717 \times 500)`.
|
||||
- ``edge_list``: The edge list. ``List`` with length :math:`7,963`.
|
||||
- ``labels``: The label list. ``torch.LongTensor`` with size :math:`(19,717, )`.
|
||||
- ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(19,717, )`.
|
||||
- ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(19,717, )`.
|
||||
- ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(19,717, )`.
|
||||
|
||||
Args:
|
||||
``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root: Optional[str] = None) -> None:
|
||||
super().__init__("cocitation_pubmed", data_root)
|
||||
self._content = {
|
||||
"num_classes": 3,
|
||||
"num_vertices": 19717,
|
||||
"num_edges": 7963,
|
||||
"dim_features": 500,
|
||||
"features": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "features.pkl",
|
||||
"md5": "f89502c432ca451156a8235c5efc034e",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_tensor, partial(norm_ft, ord=1)],
|
||||
},
|
||||
"edge_list": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "edge_list.pkl",
|
||||
"md5": "c5fbedf63e5be527f200e8c4e0391b00",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
},
|
||||
"labels": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "labels.pkl",
|
||||
"md5": "c039f778409a15f9b2ceefacad9c2202",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
"train_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "train_mask.pkl",
|
||||
"md5": "81b422937f3adccd89a334d7093b67d7",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"val_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "val_mask.pkl",
|
||||
"md5": "10717940ddbfa3e4f6c0b148bb394f79",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"test_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "test_mask.pkl",
|
||||
"md5": "10717940ddbfa3e4f6c0b148bb394f79",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import requests
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
def request_text_from_url(url):
|
||||
"""Requests text data from the specified URL.
|
||||
|
||||
Args:
|
||||
url (str): The URL from which to request data.
|
||||
|
||||
Returns:
|
||||
str: The text content of the response if the request is successful.
|
||||
|
||||
Raises:
|
||||
EasyGraphError: If a connection error occurs or the HTTP response status code indicates failure.
|
||||
"""
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
raise EasyGraphError("Connection Error!")
|
||||
|
||||
if r.ok:
|
||||
return r.text
|
||||
else:
|
||||
raise EasyGraphError(f"Error: HTTP response {r.status_code}")
|
||||
|
||||
|
||||
class contact_primary_school:
|
||||
"""A class for loading and processing the primary school contact network hypergraph dataset.
|
||||
|
||||
This class loads hyperedge, node label, and label name data from specified URLs and generates a hypergraph.
|
||||
|
||||
Attributes:
|
||||
data_root (str): The root URL for the data. If not provided, it is set to None.
|
||||
hyperedges_path (str): The URL for the hyperedge data.
|
||||
node_labels_path (str): The URL for the node label data.
|
||||
label_names_path (str): The URL for the label name data.
|
||||
_hyperedges (list): A list storing hyperedges.
|
||||
_node_labels (list): A list storing node labels.
|
||||
_label_names (list): A list storing label names.
|
||||
_node_names (list): A list storing node names (currently unused).
|
||||
_content (dict): A dictionary containing dataset statistics and data.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root=None):
|
||||
"""Initializes an instance of the contact_primary_school class.
|
||||
|
||||
Args:
|
||||
data_root (str, optional): The root URL for the data. Defaults to None.
|
||||
"""
|
||||
self.data_root = "https://" if data_root is not None else data_root
|
||||
self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-contact-primary-school/-/raw/main/hyperedges-contact-primary-school.txt?inline=false"
|
||||
self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-contact-primary-school/-/raw/main/node-labels-contact-primary-school.txt?ref_type=heads&inline=false"
|
||||
# self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false"
|
||||
self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-contact-primary-school/-/raw/main/label-names-contact-primary-school.txt?ref_type=heads&inline=false"
|
||||
self._hyperedges = []
|
||||
self._node_labels = []
|
||||
self._label_names = []
|
||||
self._node_names = []
|
||||
self.generate_hypergraph(
|
||||
hyperedges_path=self.hyperedges_path,
|
||||
node_labels_path=self.node_labels_path,
|
||||
# node_names_path=self.node_names_path,
|
||||
label_names_path=self.label_names_path,
|
||||
)
|
||||
self._content = {
|
||||
"num_classes": len(self._label_names),
|
||||
"num_vertices": len(self._node_labels),
|
||||
"num_edges": len(self._hyperedges),
|
||||
"edge_list": self._hyperedges,
|
||||
"labels": self._node_labels,
|
||||
}
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
"""Accesses data in the _content dictionary by key.
|
||||
|
||||
Args:
|
||||
key (str): The key of the data to access.
|
||||
|
||||
Returns:
|
||||
Any: The value corresponding to the key in the _content dictionary.
|
||||
"""
|
||||
return self._content[key]
|
||||
|
||||
def process_label_txt(self, data_str, delimiter="\n", transform_fun=str):
|
||||
"""Processes label data read from a text file.
|
||||
|
||||
Args:
|
||||
data_str (str): A string containing label data.
|
||||
delimiter (str, optional): The delimiter used to split the string. Defaults to "\n".
|
||||
transform_fun (callable, optional): A function used to transform each label. Defaults to str.
|
||||
|
||||
Returns:
|
||||
list: A list of processed labels.
|
||||
"""
|
||||
data_str = data_str.strip()
|
||||
data_lst = data_str.split(delimiter)
|
||||
final_lst = []
|
||||
for data in data_lst:
|
||||
data = data.strip()
|
||||
data = transform_fun(data)
|
||||
final_lst.append(data)
|
||||
return final_lst
|
||||
|
||||
@property
|
||||
def node_labels(self):
|
||||
"""Gets the list of node labels.
|
||||
|
||||
Returns:
|
||||
list: A list of node labels.
|
||||
"""
|
||||
return self._node_labels
|
||||
|
||||
"""
|
||||
@property
|
||||
def node_names(self):
|
||||
return self._node_names
|
||||
"""
|
||||
|
||||
@property
|
||||
def label_names(self):
|
||||
"""Gets the list of label names.
|
||||
|
||||
Returns:
|
||||
list: A list of label names.
|
||||
"""
|
||||
return self._label_names
|
||||
|
||||
@property
|
||||
def hyperedges(self):
|
||||
"""Gets the list of hyperedges.
|
||||
|
||||
Returns:
|
||||
list: A list of hyperedges.
|
||||
"""
|
||||
return self._hyperedges
|
||||
|
||||
def generate_hypergraph(
|
||||
self,
|
||||
hyperedges_path=None,
|
||||
node_labels_path=None,
|
||||
# node_names_path=None,
|
||||
label_names_path=None,
|
||||
):
|
||||
"""Generates hypergraph data from specified URLs.
|
||||
|
||||
Args:
|
||||
hyperedges_path (str, optional): The URL for the hyperedge data. Defaults to None.
|
||||
node_labels_path (str, optional): The URL for the node label data. Defaults to None.
|
||||
label_names_path (str, optional): The URL for the label name data. Defaults to None.
|
||||
"""
|
||||
|
||||
def fun(data):
|
||||
"""Converts the input data to an integer and subtracts 1.
|
||||
|
||||
Args:
|
||||
data (str): The input string data.
|
||||
|
||||
Returns:
|
||||
int: The converted integer data.
|
||||
"""
|
||||
data = int(data) - 1
|
||||
return data
|
||||
|
||||
hyperedges_info = request_text_from_url(hyperedges_path)
|
||||
hyperedges_info = hyperedges_info.strip()
|
||||
hyperedges_lst = hyperedges_info.split("\n")
|
||||
for hyperedge in hyperedges_lst:
|
||||
hyperedge = hyperedge.strip()
|
||||
hyperedge = [int(i) - 1 for i in hyperedge.split(",")]
|
||||
self._hyperedges.append(tuple(hyperedge))
|
||||
# print(self.hyperedges)
|
||||
|
||||
node_labels_info = request_text_from_url(node_labels_path)
|
||||
|
||||
process_node_labels_info = self.process_label_txt(
|
||||
node_labels_info, transform_fun=fun
|
||||
)
|
||||
self._node_labels = process_node_labels_info
|
||||
label_names_info = request_text_from_url(label_names_path)
|
||||
process_label_names_info = self.process_label_txt(label_names_info)
|
||||
self._label_names = process_label_names_info
|
||||
@@ -0,0 +1,85 @@
|
||||
from typing import Optional
|
||||
|
||||
from easygraph.datapipe import load_from_pickle
|
||||
from easygraph.datapipe import to_bool_tensor
|
||||
from easygraph.datapipe import to_long_tensor
|
||||
from easygraph.datasets.hypergraph.hypergraph_dataset_base import BaseData
|
||||
|
||||
|
||||
class Cooking200(BaseData):
|
||||
r"""The Cooking 200 dataset is collected from `Yummly.com <https://www.yummly.com/>`_ for vertex classification task.
|
||||
It is a hypergraph dataset, in which vertex denotes the dish and hyperedge denotes
|
||||
the ingredient. Each dish is also associated with category information, which indicates the dish's cuisine like
|
||||
Chinese, Japanese, French, and Russian.
|
||||
|
||||
The content of the Cooking200 dataset includes the following:
|
||||
|
||||
- ``num_classes``: The number of classes: :math:`20`.
|
||||
- ``num_vertices``: The number of vertices: :math:`7,403`.
|
||||
- ``num_edges``: The number of edges: :math:`2,755`.
|
||||
- ``edge_list``: The edge list. ``List`` with length :math:`(2,755)`.
|
||||
- ``labels``: The label list. ``torch.LongTensor`` with size :math:`(7,403)`.
|
||||
- ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(7,403)`.
|
||||
- ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(7,403)`.
|
||||
- ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(7,403)`.
|
||||
|
||||
Args:
|
||||
``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root: Optional[str] = None) -> None:
|
||||
super().__init__("cooking_200", data_root)
|
||||
self._content = {
|
||||
"num_classes": 20,
|
||||
"num_vertices": 7403,
|
||||
"num_edges": 2755,
|
||||
"edge_list": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "edge_list.pkl",
|
||||
"md5": "2cd32e13dd4e33576c43936542975220",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
},
|
||||
"labels": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "labels.pkl",
|
||||
"md5": "f1f3c0399c9c28547088f44e0bfd5c81",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_long_tensor],
|
||||
},
|
||||
"train_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "train_mask.pkl",
|
||||
"md5": "66ea36bae024aaaed289e1998fe894bd",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"val_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "val_mask.pkl",
|
||||
"md5": "6c0d3d8b752e3955c64788cc65dcd018",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
"test_mask": {
|
||||
"upon": [
|
||||
{
|
||||
"filename": "test_mask.pkl",
|
||||
"md5": "0e1564904551ba493e1f8a09d103461e",
|
||||
}
|
||||
],
|
||||
"loader": load_from_pickle,
|
||||
"preprocess": [to_bool_tensor],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
from easygraph.datapipe import compose_pipes
|
||||
from easygraph.datasets.hypergraph._global import DATASETS_ROOT
|
||||
from easygraph.datasets.hypergraph._global import REMOTE_DATASETS_ROOT
|
||||
from easygraph.datasets.utils import download_and_check
|
||||
|
||||
|
||||
class BaseData:
|
||||
r"""The Base Class of all datasets.
|
||||
|
||||
::
|
||||
|
||||
self._content = {
|
||||
'item': {
|
||||
'upon': [
|
||||
{'filename': 'part1.pkl', 'md5': 'xxxxx',},
|
||||
{'filename': 'part2.pkl', 'md5': 'xxxxx',},
|
||||
],
|
||||
'loader': loader_function,
|
||||
'preprocess': [datapipe1, datapipe2],
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, data_root=None):
|
||||
# configure the data local/remote root
|
||||
self.name = name
|
||||
if data_root is None:
|
||||
self.data_root = DATASETS_ROOT / name
|
||||
else:
|
||||
self.data_root = Path(data_root) / name
|
||||
self.remote_root = REMOTE_DATASETS_ROOT + name + "/"
|
||||
# init
|
||||
self._content = {}
|
||||
self._raw = {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"This is {self.name} dataset:\n"
|
||||
+ "\n".join(f" -> {k}" for k in self.content)
|
||||
+ "\nPlease try `data['name']` to get the specified data."
|
||||
)
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
r"""Return the content of the dataset."""
|
||||
return list(self._content.keys())
|
||||
|
||||
def needs_to_load(self, item_name: str) -> bool:
|
||||
r"""Return whether the ``item_name`` of the dataset needs to be loaded.
|
||||
|
||||
Args:
|
||||
``item_name`` (``str``): The name of the item in the dataset.
|
||||
"""
|
||||
assert item_name in self.content, f"{item_name} is not provided in the Data"
|
||||
return (
|
||||
isinstance(self._content[item_name], dict)
|
||||
and "upon" in self._content[item_name]
|
||||
and "loader" in self._content[item_name]
|
||||
)
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
if self.needs_to_load(key):
|
||||
cur_cfg = self._content[key]
|
||||
if cur_cfg.get("cache", None) is None:
|
||||
# get raw data
|
||||
item = self.raw(key)
|
||||
# preprocess and cache
|
||||
pipes = cur_cfg.get("preprocess", None)
|
||||
if pipes is not None:
|
||||
cur_cfg["cache"] = compose_pipes(*pipes)(item)
|
||||
else:
|
||||
cur_cfg["cache"] = item
|
||||
return cur_cfg["cache"]
|
||||
else:
|
||||
return self._content[key]
|
||||
|
||||
def raw(self, key: str) -> Any:
|
||||
r"""Return the ``key`` of the dataset with un-preprocessed format."""
|
||||
if self.needs_to_load(key):
|
||||
cur_cfg = self._content[key]
|
||||
if self._raw.get(key, None) is None:
|
||||
upon = cur_cfg["upon"]
|
||||
if len(upon) == 0:
|
||||
return None
|
||||
self.fetch_files(cur_cfg["upon"])
|
||||
file_path_list = [
|
||||
self.data_root / u["filename"] for u in cur_cfg["upon"]
|
||||
]
|
||||
if len(file_path_list) == 1:
|
||||
self._raw[key] = cur_cfg["loader"](file_path_list[0])
|
||||
else:
|
||||
# here, you should implement a multi-file loader
|
||||
self._raw[key] = cur_cfg["loader"](file_path_list)
|
||||
return self._raw[key]
|
||||
else:
|
||||
return self._content[key]
|
||||
|
||||
def fetch_files(self, files: List[Dict[str, str]]):
|
||||
r"""Download and check the files if they are not exist.
|
||||
|
||||
Args:
|
||||
``files`` (``List[Dict[str, str]]``): The files to download, each element
|
||||
in the list is a dict with at lease two keys: ``filename`` and ``md5``.
|
||||
If extra key ``bk_url`` is provided, it will be used to download the
|
||||
file from the backup url.
|
||||
"""
|
||||
for file in files:
|
||||
cur_filename = file["filename"]
|
||||
cur_url = file.get("bk_url", None)
|
||||
if cur_url is None:
|
||||
cur_url = self.remote_root + cur_filename
|
||||
download_and_check(cur_url, self.data_root / cur_filename, file["md5"])
|
||||
@@ -0,0 +1,78 @@
|
||||
import os.path as osp
|
||||
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
import torch
|
||||
|
||||
from torch_geometric.data import Data
|
||||
from torch_sparse import coalesce
|
||||
|
||||
|
||||
__all__ = ["load_line_expansion_dataset"]
|
||||
|
||||
|
||||
def load_line_expansion_dataset(
|
||||
path=None, dataset="cocitation-cora", train_percent=0.5
|
||||
):
|
||||
# load edges, features, and labels.
|
||||
print("Loading {} dataset...".format(dataset))
|
||||
|
||||
file_name = f"{dataset}.content"
|
||||
p2idx_features_labels = osp.join(path, dataset, file_name)
|
||||
idx_features_labels = np.genfromtxt(p2idx_features_labels, dtype=np.dtype(str))
|
||||
# features = np.array(idx_features_labels[:, 1:-1])
|
||||
features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)
|
||||
# labels = encode_onehot(idx_features_labels[:, -1])
|
||||
labels = torch.LongTensor(idx_features_labels[:, -1].astype(float))
|
||||
|
||||
print("load features")
|
||||
|
||||
# build graph
|
||||
idx = np.array(idx_features_labels[:, 0], dtype=np.int32)
|
||||
idx_map = {j: i for i, j in enumerate(idx)}
|
||||
|
||||
file_name = f"{dataset}.edges"
|
||||
p2edges_unordered = osp.join(path, dataset, file_name)
|
||||
edges_unordered = np.genfromtxt(p2edges_unordered, dtype=np.int32)
|
||||
|
||||
edges = np.array(
|
||||
list(map(idx_map.get, edges_unordered.flatten())), dtype=np.int32
|
||||
).reshape(edges_unordered.shape)
|
||||
|
||||
print("load edges")
|
||||
|
||||
# From adjacency matrix to edge_list
|
||||
edge_index = edges.T
|
||||
# ipdb.set_trace()
|
||||
assert edge_index[0].max() == edge_index[1].min() - 1
|
||||
|
||||
# check if values in edge_index is consecutive. i.e. no missing value for node_id/he_id.
|
||||
assert len(np.unique(edge_index)) == edge_index.max() + 1
|
||||
|
||||
num_nodes = edge_index[0].max() + 1
|
||||
num_he = edge_index[1].max() - num_nodes + 1
|
||||
edge_index = np.hstack((edge_index, edge_index[::-1, :]))
|
||||
|
||||
# build torch data class
|
||||
data = Data(
|
||||
x=torch.FloatTensor(np.array(features[:num_nodes].todense())),
|
||||
edge_index=torch.LongTensor(edge_index),
|
||||
y=labels[:num_nodes],
|
||||
)
|
||||
|
||||
# used user function to override the default function.
|
||||
# the following will also sort the edge_index and remove duplicates.
|
||||
total_num_node_id_he_id = len(np.unique(edge_index))
|
||||
data.edge_index, data.edge_attr = coalesce(
|
||||
data.edge_index, None, total_num_node_id_he_id, total_num_node_id_he_id
|
||||
)
|
||||
n_x = num_nodes
|
||||
# n_x = n_expanded
|
||||
num_class = len(np.unique(labels[:num_nodes].numpy()))
|
||||
data.n_x = n_x
|
||||
# add parameters to attribute
|
||||
|
||||
data.train_percent = train_percent
|
||||
data.num_hyperedges = num_he
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,113 @@
|
||||
import requests
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
def request_text_from_url(url):
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
raise EasyGraphError("Connection Error!")
|
||||
|
||||
if r.ok:
|
||||
return r.text
|
||||
else:
|
||||
raise EasyGraphError(f"Error: HTTP response {r.status_code}")
|
||||
|
||||
|
||||
class mathoverflow_answers:
|
||||
def __init__(self, data_root=None):
|
||||
self.data_root = "https://" if data_root is not None else data_root
|
||||
self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-mathoverflow-answers/-/raw/main/hyperedges-mathoverflow-answers.txt?inline=false"
|
||||
self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-mathoverflow-answers/-/raw/main/node-labels-mathoverflow-answers.txt?ref_type=heads&inline=false"
|
||||
# self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false"
|
||||
self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-mathoverflow-answers/-/raw/main/label-names-mathoverflow-answers.txt?ref_type=heads&inline=false"
|
||||
self._hyperedges = []
|
||||
self._node_labels = []
|
||||
self._label_names = []
|
||||
self._node_names = []
|
||||
self.generate_hypergraph(
|
||||
hyperedges_path=self.hyperedges_path,
|
||||
node_labels_path=self.node_labels_path,
|
||||
# node_names_path=self.node_names_path,
|
||||
label_names_path=self.label_names_path,
|
||||
)
|
||||
self._content = {
|
||||
"num_classes": len(self._label_names),
|
||||
"num_vertices": len(self._node_labels),
|
||||
"num_edges": len(self._hyperedges),
|
||||
"edge_list": self._hyperedges,
|
||||
"labels": self._node_labels,
|
||||
}
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
return self._content[key]
|
||||
|
||||
def process_label_txt(self, data_str, delimiter="\n", transform_fun=str):
|
||||
data_str = data_str.strip()
|
||||
data_lst = data_str.split(delimiter)
|
||||
final_lst = []
|
||||
for data in data_lst:
|
||||
data = data.strip()
|
||||
data = transform_fun(data)
|
||||
final_lst.append(data)
|
||||
return final_lst
|
||||
|
||||
@property
|
||||
def node_labels(self):
|
||||
return self._node_labels
|
||||
|
||||
"""
|
||||
@property
|
||||
def node_names(self):
|
||||
return self._node_names
|
||||
"""
|
||||
|
||||
@property
|
||||
def label_names(self):
|
||||
return self._label_names
|
||||
|
||||
@property
|
||||
def hyperedges(self):
|
||||
return self._hyperedges
|
||||
|
||||
def generate_hypergraph(
|
||||
self,
|
||||
hyperedges_path=None,
|
||||
node_labels_path=None,
|
||||
# node_names_path=None,
|
||||
label_names_path=None,
|
||||
):
|
||||
def fun(data):
|
||||
data = int(data) - 1
|
||||
return data
|
||||
|
||||
hyperedges_info = request_text_from_url(hyperedges_path)
|
||||
hyperedges_info = hyperedges_info.strip()
|
||||
hyperedges_lst = hyperedges_info.split("\n")
|
||||
for hyperedge in hyperedges_lst:
|
||||
hyperedge = hyperedge.strip()
|
||||
hyperedge = [int(i) - 1 for i in hyperedge.split(",")]
|
||||
self._hyperedges.append(tuple(hyperedge))
|
||||
# print(self.hyperedges)
|
||||
"""
|
||||
node_labels_info = request_text_from_url(node_labels_path)
|
||||
|
||||
process_node_labels_info = self.process_label_txt(
|
||||
node_labels_info, transform_fun=fun
|
||||
)
|
||||
self._node_labels = process_node_labels_info
|
||||
"""
|
||||
node_labels_info = request_text_from_url(node_labels_path)
|
||||
node_labels_info = node_labels_info.strip()
|
||||
node_labels_lst = node_labels_info.split("\n")
|
||||
for node_label in node_labels_lst:
|
||||
node_label = node_label.strip()
|
||||
node_label = [int(i) - 1 for i in node_label.split(",")]
|
||||
self._node_labels.append(tuple(node_label))
|
||||
# print("process_node_labels_info:", process_node_labels_info)
|
||||
# print("process_node_names_info:", process_node_names_info)
|
||||
label_names_info = request_text_from_url(label_names_path)
|
||||
process_label_names_info = self.process_label_txt(label_names_info)
|
||||
self._label_names = process_label_names_info
|
||||
# print("process_label_names_info:", process_label_names_info)
|
||||
@@ -0,0 +1,106 @@
|
||||
import requests
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
def request_text_from_url(url):
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
raise EasyGraphError("Connection Error!")
|
||||
|
||||
if r.ok:
|
||||
return r.text
|
||||
else:
|
||||
raise EasyGraphError(f"Error: HTTP response {r.status_code}")
|
||||
|
||||
|
||||
class senate_committees:
|
||||
def __init__(self, data_root=None):
|
||||
self.data_root = "https://" if data_root is not None else data_root
|
||||
self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-senate-committees/-/raw/main/hyperedges-senate-committees.txt?inline=false"
|
||||
self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-senate-committees/-/raw/main/node-labels-senate-committees.txt?ref_type=heads&inline=false"
|
||||
self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-senate-committees/-/raw/main/node-names-senate-committees.txt?ref_type=heads&inline=false"
|
||||
self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-senate-committees/-/raw/main/label-names-senate-committees.txt?ref_type=heads&inline=false"
|
||||
self._hyperedges = []
|
||||
self._node_labels = []
|
||||
self._label_names = []
|
||||
self._node_names = []
|
||||
self.generate_hypergraph(
|
||||
hyperedges_path=self.hyperedges_path,
|
||||
node_labels_path=self.node_labels_path,
|
||||
node_names_path=self.node_names_path,
|
||||
label_names_path=self.label_names_path,
|
||||
)
|
||||
self._content = {
|
||||
"num_classes": len(self._label_names),
|
||||
"num_vertices": len(self._node_labels),
|
||||
"num_edges": len(self._hyperedges),
|
||||
"edge_list": self._hyperedges,
|
||||
"labels": self._node_labels,
|
||||
}
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
return self._content[key]
|
||||
|
||||
def process_label_txt(self, data_str, delimiter="\n", transform_fun=str):
|
||||
data_str = data_str.strip()
|
||||
data_lst = data_str.split(delimiter)
|
||||
final_lst = []
|
||||
for data in data_lst:
|
||||
data = data.strip()
|
||||
data = transform_fun(data)
|
||||
final_lst.append(data)
|
||||
return final_lst
|
||||
|
||||
@property
|
||||
def node_labels(self):
|
||||
return self._node_labels
|
||||
|
||||
@property
|
||||
def node_names(self):
|
||||
return self._node_names
|
||||
|
||||
@property
|
||||
def label_names(self):
|
||||
return self._label_names
|
||||
|
||||
@property
|
||||
def hyperedges(self):
|
||||
return self._hyperedges
|
||||
|
||||
def generate_hypergraph(
|
||||
self,
|
||||
hyperedges_path=None,
|
||||
node_labels_path=None,
|
||||
node_names_path=None,
|
||||
label_names_path=None,
|
||||
):
|
||||
def fun(data):
|
||||
data = int(data) - 1
|
||||
return data
|
||||
|
||||
hyperedges_info = request_text_from_url(hyperedges_path)
|
||||
hyperedges_info = hyperedges_info.strip()
|
||||
hyperedges_lst = hyperedges_info.split("\n")
|
||||
for hyperedge in hyperedges_lst:
|
||||
hyperedge = hyperedge.strip()
|
||||
hyperedge = [int(i) - 1 for i in hyperedge.split(",")]
|
||||
self._hyperedges.append(tuple(hyperedge))
|
||||
# print(self.hyperedges)
|
||||
|
||||
node_labels_info = request_text_from_url(node_labels_path)
|
||||
|
||||
process_node_labels_info = self.process_label_txt(
|
||||
node_labels_info, transform_fun=fun
|
||||
)
|
||||
self._node_labels = process_node_labels_info
|
||||
# print("process_node_labels_info:", process_node_labels_info)
|
||||
node_names_info = request_text_from_url(node_names_path)
|
||||
process_node_names_info = self.process_label_txt(node_names_info)
|
||||
self._node_names = process_node_names_info
|
||||
# print("process_node_names_info:", process_node_names_info)
|
||||
label_names_info = request_text_from_url(label_names_path)
|
||||
process_label_names_info = self.process_label_txt(label_names_info)
|
||||
self._label_names = process_label_names_info
|
||||
# print("process_label_names_info:", process_label_names_info)
|
||||
@@ -0,0 +1,104 @@
|
||||
import requests
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
def request_text_from_url(url):
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
raise EasyGraphError("Connection Error!")
|
||||
|
||||
if r.ok:
|
||||
return r.text
|
||||
else:
|
||||
raise EasyGraphError(f"Error: HTTP response {r.status_code}")
|
||||
|
||||
|
||||
class trivago_clicks:
|
||||
def __init__(self, data_root=None):
|
||||
self.data_root = "https://" if data_root is not None else data_root
|
||||
self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-trivago-clicks/-/raw/main/hyperedges-trivago-clicks.txt?inline=false"
|
||||
self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-trivago-clicks/-/raw/main/node-labels-trivago-clicks.txt?ref_type=heads&inline=false"
|
||||
# self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-trivago-clicks/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false"
|
||||
self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-trivago-clicks/-/raw/main/label-names-trivago-clicks.txt?ref_type=heads&inline=false"
|
||||
self._hyperedges = []
|
||||
self._node_labels = []
|
||||
self._label_names = []
|
||||
self._node_names = []
|
||||
self.generate_hypergraph(
|
||||
hyperedges_path=self.hyperedges_path,
|
||||
node_labels_path=self.node_labels_path,
|
||||
# node_names_path=self.node_names_path,
|
||||
label_names_path=self.label_names_path,
|
||||
)
|
||||
self._content = {
|
||||
"num_classes": len(self._label_names),
|
||||
"num_vertices": len(self._node_labels),
|
||||
"num_edges": len(self._hyperedges),
|
||||
"edge_list": self._hyperedges,
|
||||
"labels": self._node_labels,
|
||||
}
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
return self._content[key]
|
||||
|
||||
def process_label_txt(self, data_str, delimiter="\n", transform_fun=str):
|
||||
data_str = data_str.strip()
|
||||
data_lst = data_str.split(delimiter)
|
||||
final_lst = []
|
||||
for data in data_lst:
|
||||
data = data.strip()
|
||||
data = transform_fun(data)
|
||||
final_lst.append(data)
|
||||
return final_lst
|
||||
|
||||
@property
|
||||
def node_labels(self):
|
||||
return self._node_labels
|
||||
|
||||
"""
|
||||
@property
|
||||
def node_names(self):
|
||||
return self._node_names
|
||||
"""
|
||||
|
||||
@property
|
||||
def label_names(self):
|
||||
return self._label_names
|
||||
|
||||
@property
|
||||
def hyperedges(self):
|
||||
return self._hyperedges
|
||||
|
||||
def generate_hypergraph(
|
||||
self,
|
||||
hyperedges_path=None,
|
||||
node_labels_path=None,
|
||||
# node_names_path=None,
|
||||
label_names_path=None,
|
||||
):
|
||||
def fun(data):
|
||||
data = int(data) - 1
|
||||
return data
|
||||
|
||||
hyperedges_info = request_text_from_url(hyperedges_path)
|
||||
hyperedges_info = hyperedges_info.strip()
|
||||
hyperedges_lst = hyperedges_info.split("\n")
|
||||
for hyperedge in hyperedges_lst:
|
||||
hyperedge = hyperedge.strip()
|
||||
hyperedge = [int(i) - 1 for i in hyperedge.split(",")]
|
||||
self._hyperedges.append(tuple(hyperedge))
|
||||
# print(self.hyperedges)
|
||||
|
||||
node_labels_info = request_text_from_url(node_labels_path)
|
||||
|
||||
process_node_labels_info = self.process_label_txt(
|
||||
node_labels_info, transform_fun=fun
|
||||
)
|
||||
self._node_labels = process_node_labels_info
|
||||
# print("process_node_labels_info:", process_node_labels_info)
|
||||
# print("process_node_names_info:", process_node_names_info)
|
||||
label_names_info = request_text_from_url(label_names_path)
|
||||
process_label_names_info = self.process_label_txt(label_names_info)
|
||||
self._label_names = process_label_names_info
|
||||
@@ -0,0 +1,208 @@
|
||||
import requests
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
def request_text_from_url(url):
|
||||
"""
|
||||
Requests text content from the given URL.
|
||||
|
||||
Args:
|
||||
url (str): The URL from which to request text data.
|
||||
|
||||
Returns:
|
||||
str: The text content of the response if the request is successful.
|
||||
|
||||
Raises:
|
||||
EasyGraphError: If a connection error occurs during the request or if the HTTP response status code is not OK.
|
||||
"""
|
||||
try:
|
||||
r = requests.get(url)
|
||||
except requests.ConnectionError:
|
||||
raise EasyGraphError("Connection Error!")
|
||||
|
||||
if r.ok:
|
||||
return r.text
|
||||
else:
|
||||
raise EasyGraphError(f"Error: HTTP response {r.status_code}")
|
||||
|
||||
|
||||
class walmart_trips:
|
||||
"""
|
||||
A class for loading and processing the Walmart trips hypergraph dataset.
|
||||
|
||||
This class fetches hyperedge, node label, and label name data from predefined URLs,
|
||||
processes the data, and generates a hypergraph representation. It also provides access
|
||||
to various dataset attributes through properties and indexing.
|
||||
|
||||
Attributes:
|
||||
data_root (str): The root URL for the data. If provided during initialization, it is set to "https://";
|
||||
otherwise, it is None.
|
||||
hyperedges_path (str): The URL of the file containing hyperedge information.
|
||||
node_labels_path (str): The URL of the file containing node label information.
|
||||
label_names_path (str): The URL of the file containing label name information.
|
||||
_hyperedges (list): A list of tuples representing hyperedges.
|
||||
_node_labels (list): A list of node labels.
|
||||
_label_names (list): A list of label names.
|
||||
_node_names (list): An empty list reserved for node names (currently unused).
|
||||
_content (dict): A dictionary containing dataset statistics and data, such as the number of classes,
|
||||
vertices, edges, the edge list, and node labels.
|
||||
"""
|
||||
|
||||
def __init__(self, data_root=None, local_path=None):
|
||||
"""
|
||||
Initializes an instance of the walmart_trips class.
|
||||
|
||||
Args:
|
||||
data_root (str, optional): The root URL for the data. If provided, it is set to "https://";
|
||||
otherwise, it is None. Defaults to None.
|
||||
local_path (str, optional): Currently unused. Defaults to None.
|
||||
"""
|
||||
self.data_root = "https://" if data_root is not None else data_root
|
||||
self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-walmart-trips/-/raw/main/hyperedges-walmart-trips.txt?inline=false"
|
||||
self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-walmart-trips/-/raw/main/node-labels-walmart-trips.txt?ref_type=heads&inline=false"
|
||||
# self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-walmart-trips/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false"
|
||||
self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-walmart-trips/-/raw/main/label-names-walmart-trips.txt?ref_type=heads&inline=false"
|
||||
self._hyperedges = []
|
||||
self._node_labels = []
|
||||
self._label_names = []
|
||||
self._node_names = []
|
||||
|
||||
self.generate_hypergraph(
|
||||
hyperedges_path=self.hyperedges_path,
|
||||
node_labels_path=self.node_labels_path,
|
||||
# node_names_path=self.node_names_path,
|
||||
label_names_path=self.label_names_path,
|
||||
)
|
||||
|
||||
self._content = {
|
||||
"num_classes": len(self._label_names),
|
||||
"num_vertices": len(self._node_labels),
|
||||
"num_edges": len(self._hyperedges),
|
||||
"edge_list": self._hyperedges,
|
||||
"labels": self._node_labels,
|
||||
}
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
"""
|
||||
Retrieves a value from the _content dictionary using the specified key.
|
||||
|
||||
Args:
|
||||
key (str): The key used to access the _content dictionary.
|
||||
|
||||
Returns:
|
||||
Any: The value corresponding to the key in the _content dictionary.
|
||||
"""
|
||||
return self._content[key]
|
||||
|
||||
def process_label_txt(self, data_str, delimiter="\n", transform_fun=str):
|
||||
"""
|
||||
Processes a string containing label data into a list of transformed values.
|
||||
|
||||
Args:
|
||||
data_str (str): The input string containing label data.
|
||||
delimiter (str, optional): The delimiter used to split the input string. Defaults to "\n".
|
||||
transform_fun (callable, optional): A function used to transform each label value.
|
||||
Defaults to the str function.
|
||||
|
||||
Returns:
|
||||
list: A list of transformed label values.
|
||||
"""
|
||||
data_str = data_str.strip()
|
||||
data_lst = data_str.split(delimiter)
|
||||
final_lst = []
|
||||
for data in data_lst:
|
||||
data = data.strip()
|
||||
data = transform_fun(data)
|
||||
final_lst.append(data)
|
||||
return final_lst
|
||||
|
||||
@property
|
||||
def node_labels(self):
|
||||
"""
|
||||
Gets the list of node labels.
|
||||
|
||||
Returns:
|
||||
list: A list of node labels.
|
||||
"""
|
||||
return self._node_labels
|
||||
|
||||
"""
|
||||
@property
|
||||
def node_names(self):
|
||||
return self._node_names
|
||||
"""
|
||||
|
||||
@property
|
||||
def label_names(self):
|
||||
"""
|
||||
Gets the list of label names.
|
||||
|
||||
Returns:
|
||||
list: A list of label names.
|
||||
"""
|
||||
return self._label_names
|
||||
|
||||
@property
|
||||
def hyperedges(self):
|
||||
"""
|
||||
Gets the list of hyperedges.
|
||||
|
||||
Returns:
|
||||
list: A list of tuples representing hyperedges.
|
||||
"""
|
||||
return self._hyperedges
|
||||
|
||||
def generate_hypergraph(
|
||||
self,
|
||||
hyperedges_path=None,
|
||||
node_labels_path=None,
|
||||
# node_names_path=None,
|
||||
label_names_path=None,
|
||||
):
|
||||
"""
|
||||
Generates a hypergraph by fetching and processing data from the specified URLs.
|
||||
|
||||
Args:
|
||||
hyperedges_path (str, optional): The URL of the file containing hyperedge information.
|
||||
Defaults to None.
|
||||
node_labels_path (str, optional): The URL of the file containing node label information.
|
||||
Defaults to None.
|
||||
label_names_path (str, optional): The URL of the file containing label name information.
|
||||
Defaults to None.
|
||||
"""
|
||||
|
||||
def fun(data):
|
||||
"""
|
||||
Converts a string to an integer and subtracts 1.
|
||||
|
||||
Args:
|
||||
data (str): The input string to be converted.
|
||||
|
||||
Returns:
|
||||
int: The converted integer value minus 1.
|
||||
"""
|
||||
data = int(data) - 1
|
||||
return data
|
||||
|
||||
hyperedges_info = request_text_from_url(hyperedges_path)
|
||||
hyperedges_info = hyperedges_info.strip()
|
||||
hyperedges_lst = hyperedges_info.split("\n")
|
||||
for hyperedge in hyperedges_lst:
|
||||
hyperedge = hyperedge.strip()
|
||||
hyperedge = [int(i) - 1 for i in hyperedge.split(",")]
|
||||
self._hyperedges.append(tuple(hyperedge))
|
||||
# print(self.hyperedges)
|
||||
|
||||
node_labels_info = request_text_from_url(node_labels_path)
|
||||
|
||||
process_node_labels_info = self.process_label_txt(
|
||||
node_labels_info, transform_fun=fun
|
||||
)
|
||||
self._node_labels = process_node_labels_info
|
||||
# print("process_node_labels_info:", process_node_labels_info)
|
||||
# print("process_node_names_info:", process_node_names_info)
|
||||
label_names_info = request_text_from_url(label_names_path)
|
||||
process_label_names_info = self.process_label_txt(label_names_info)
|
||||
self._label_names = process_label_names_info
|
||||
# print("process_label_names_info:", process_label_names_info)
|
||||
@@ -0,0 +1,93 @@
|
||||
import easygraph as eg
|
||||
|
||||
from .graph_dataset_base import EasyGraphDataset
|
||||
from .utils import _set_labels
|
||||
from .utils import tensor
|
||||
|
||||
|
||||
""" KarateClubDataset for inductive learning. """
|
||||
|
||||
|
||||
class KarateClubDataset(EasyGraphDataset):
|
||||
"""Karate Club dataset for Node Classification
|
||||
|
||||
Zachary's karate club is a social network of a university
|
||||
karate club, described in the paper "An Information Flow
|
||||
Model for Conflict and Fission in Small Groups" by Wayne W. Zachary.
|
||||
The network became a popular example of community structure in
|
||||
networks after its use by Michelle Girvan and Mark Newman in 2002.
|
||||
Official website: `<http://konect.cc/networks/ucidata-zachary/>`_
|
||||
|
||||
Karate Club dataset statistics:
|
||||
|
||||
- Nodes: 34
|
||||
- Edges: 156
|
||||
- Number of Classes: 2
|
||||
|
||||
Parameters
|
||||
----------
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~eg.Graph` object and returns
|
||||
a transformed version. The :class:`~eg.Graph` object will be
|
||||
transformed before every access.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
num_classes : int
|
||||
Number of node classes
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> dataset = KarateClubDataset()
|
||||
>>> num_classes = dataset.num_classes
|
||||
>>> g = dataset[0]
|
||||
>>> labels = g.ndata['label']
|
||||
"""
|
||||
|
||||
def __init__(self, transform=None):
|
||||
super(KarateClubDataset, self).__init__(name="karate_club", transform=transform)
|
||||
|
||||
def process(self):
|
||||
import numpy as np
|
||||
|
||||
kc_graph = eg.get_graph_karateclub()
|
||||
label = np.asarray(
|
||||
[kc_graph.nodes[i]["club"] != "Mr. Hi" for i in kc_graph.nodes]
|
||||
).astype(np.int64)
|
||||
label = tensor(label)
|
||||
|
||||
kc_graph = _set_labels(kc_graph, label)
|
||||
kc_graph.ndata["label"] = label
|
||||
self._graph = kc_graph
|
||||
self._data = [kc_graph]
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
"""Number of classes."""
|
||||
return 2
|
||||
|
||||
def __getitem__(self, idx):
|
||||
r"""Get graph object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
idx : int
|
||||
Item index, KarateClubDataset has only one graph object
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`eg.Graph`
|
||||
|
||||
graph structure and labels.
|
||||
|
||||
- ``ndata['label']``: ground truth labels
|
||||
"""
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
if self._transform is None:
|
||||
return self._graph
|
||||
else:
|
||||
return self._transform(self._graph)
|
||||
|
||||
def __len__(self):
|
||||
r"""The number of graphs in the dataset."""
|
||||
return 1
|
||||
@@ -0,0 +1,217 @@
|
||||
"""PPIDataset for inductive learning."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
from easygraph.classes.directed_graph import DiGraph
|
||||
|
||||
from ..readwrite import json_graph
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import _get_dgl_url
|
||||
from .utils import data_type_dict
|
||||
from .utils import tensor
|
||||
|
||||
|
||||
class PPIDataset(EasyGraphBuiltinDataset):
|
||||
r"""Protein-Protein Interaction dataset for inductive node classification
|
||||
|
||||
A toy Protein-Protein Interaction network dataset. The dataset contains
|
||||
24 graphs. The average number of nodes per graph is 2372. Each node has
|
||||
50 features and 121 labels. 20 graphs for training, 2 for validation
|
||||
and 2 for testing.
|
||||
|
||||
Reference: `<http://snap.stanford.edu/graphsage/>`_
|
||||
|
||||
Statistics:
|
||||
|
||||
- Train examples: 20
|
||||
- Valid examples: 2
|
||||
- Test examples: 2
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str
|
||||
Must be one of ('train', 'valid', 'test').
|
||||
Default: 'train'
|
||||
raw_dir : str
|
||||
Raw file directory to download/contains the input data directory.
|
||||
Default: ~/.eg/
|
||||
force_reload : bool
|
||||
Whether to reload the dataset.
|
||||
Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information.
|
||||
Default: True.
|
||||
transform : callable, optional
|
||||
A transform that takes in a :class:`~eg.DGLGraph` object and returns
|
||||
a transformed version. The :class:`~eg.DGLGraph` object will be
|
||||
transformed before every access.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
num_labels : int
|
||||
Number of labels for each node
|
||||
labels : Tensor
|
||||
Node labels
|
||||
features : Tensor
|
||||
Node features
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> dataset = PPIDataset(mode='valid')
|
||||
>>> num_labels = dataset.num_labels
|
||||
>>> for g in dataset:
|
||||
.... feat = g.ndata['feat']
|
||||
.... label = g.ndata['label']
|
||||
.... # your code here
|
||||
>>>
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mode="train",
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=False,
|
||||
transform=None,
|
||||
):
|
||||
assert mode in ["train", "valid", "test"]
|
||||
self.mode = mode
|
||||
_url = _get_dgl_url("dataset/ppi.zip")
|
||||
super(PPIDataset, self).__init__(
|
||||
name="ppi",
|
||||
url=_url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
graph_file = os.path.join(
|
||||
self.save_path, "ppi", "{}_graph.json".format(self.mode)
|
||||
)
|
||||
label_file = os.path.join(
|
||||
self.save_path, "ppi", "{}_labels.npy".format(self.mode)
|
||||
)
|
||||
feat_file = os.path.join(
|
||||
self.save_path, "ppi", "{}_feats.npy".format(self.mode)
|
||||
)
|
||||
graph_id_file = os.path.join(
|
||||
self.save_path, "ppi", "{}_graph_id.npy".format(self.mode)
|
||||
)
|
||||
|
||||
g_data = json.load(open(graph_file))
|
||||
self._labels = np.load(label_file)
|
||||
self._feats = np.load(feat_file)
|
||||
self.graph = DiGraph(json_graph.node_link_graph(g_data))
|
||||
graph_id = np.load(graph_id_file)
|
||||
|
||||
# lo, hi means the range of graph ids for different portion of the dataset,
|
||||
# 20 graphs for training, 2 for validation and 2 for testing.
|
||||
lo, hi = 1, 21
|
||||
if self.mode == "valid":
|
||||
lo, hi = 21, 23
|
||||
elif self.mode == "test":
|
||||
lo, hi = 23, 25
|
||||
|
||||
graph_masks = []
|
||||
self.graphs = []
|
||||
for g_id in range(lo, hi):
|
||||
g_mask = np.where(graph_id == g_id)[0]
|
||||
graph_masks.append(g_mask)
|
||||
g = self.graph.nodes_subgraph(g_mask)
|
||||
g.ndata["feat"] = tensor(
|
||||
self._feats[g_mask], dtype=data_type_dict()["float32"]
|
||||
)
|
||||
g.ndata["label"] = tensor(
|
||||
self._labels[g_mask], dtype=data_type_dict()["float32"]
|
||||
)
|
||||
self.graphs.append(g)
|
||||
|
||||
def has_cache(self):
|
||||
graph_list_path = os.path.join(
|
||||
self.save_path, "{}_eg_graph_list.bin".format(self.mode)
|
||||
)
|
||||
g_path = os.path.join(self.save_path, "{}_eg_graph.bin".format(self.mode))
|
||||
info_path = os.path.join(self.save_path, "{}_info.pkl".format(self.mode))
|
||||
return (
|
||||
os.path.exists(graph_list_path)
|
||||
and os.path.exists(g_path)
|
||||
and os.path.exists(info_path)
|
||||
)
|
||||
|
||||
def save(self):
|
||||
graph_list_path = os.path.join(
|
||||
self.save_path, "{}_eg_graph_list.bin".format(self.mode)
|
||||
)
|
||||
g_path = os.path.join(self.save_path, "{}_eg_graph.bin".format(self.mode))
|
||||
info_path = os.path.join(self.save_path, "{}_info.pkl".format(self.mode))
|
||||
# save_graphs(graph_list_path, self.graphs)
|
||||
# save_graphs(g_path, self.graph)
|
||||
# save_info(info_path, {'labels': self._labels, 'feats': self._feats})
|
||||
|
||||
# def load(self):
|
||||
# graph_list_path = os.path.join(self.save_path, '{}_eg_graph_list.bin'.format(self.mode))
|
||||
# g_path = os.path.join(self.save_path, '{}_eg_graph.bin'.format(self.mode))
|
||||
# info_path = os.path.join(self.save_path, '{}_info.pkl'.format(self.mode))
|
||||
# self.graphs = load_graphs(graph_list_path)[0]
|
||||
# g, _ = load_graphs(g_path)
|
||||
# self.graph = g[0]
|
||||
# info = load_info(info_path)
|
||||
# self._labels = info['labels']
|
||||
# self._feats = info['feats']
|
||||
|
||||
@property
|
||||
def num_labels(self):
|
||||
return 121
|
||||
|
||||
def __len__(self):
|
||||
"""Return number of samples in this dataset."""
|
||||
return len(self.graphs)
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Get the item^th sample.
|
||||
|
||||
Parameters
|
||||
---------
|
||||
item : int
|
||||
The sample index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`eg.Graph`
|
||||
graph structure, node features and node labels.
|
||||
|
||||
- ``ndata['feat']``: node features
|
||||
- ``ndata['label']``: node labels
|
||||
"""
|
||||
if self._transform is None:
|
||||
return self.graphs[item]
|
||||
else:
|
||||
return self._transform(self.graphs[item])
|
||||
|
||||
|
||||
class LegacyPPIDataset(PPIDataset):
|
||||
"""Legacy version of PPI Dataset"""
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Get the item^th sample.
|
||||
|
||||
Parameters
|
||||
---------
|
||||
idx : int
|
||||
The sample index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(eg.DGLGraph, Tensor, Tensor)
|
||||
The graph, features and its label.
|
||||
"""
|
||||
if self._transform is None:
|
||||
g = self.graphs[item]
|
||||
else:
|
||||
g = self._transform(self.graphs[item])
|
||||
return g, g.ndata["feat"], g.ndata["label"]
|
||||
@@ -0,0 +1,104 @@
|
||||
import os
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import data_type_dict
|
||||
from .utils import download
|
||||
from .utils import extract_archive
|
||||
from .utils import tensor
|
||||
|
||||
|
||||
class RedditDataset(EasyGraphBuiltinDataset):
|
||||
r"""Reddit posts graph (Sept 2014) for community (subreddit) classification.
|
||||
|
||||
Statistics:
|
||||
- Nodes: ~232,965
|
||||
- Edges: ~114 million (approx.)
|
||||
- Features per node: 602
|
||||
- Classes: number of subreddit communities
|
||||
|
||||
Data are split by post-day: first 20 days train, then validation (30%), test (rest).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
self_loop : bool
|
||||
Add self-loop edges if True.
|
||||
raw_dir, force_reload, verbose, transform : same as EasyGraphBuiltinDataset
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
self_loop=False,
|
||||
raw_dir=None,
|
||||
force_reload=False,
|
||||
verbose=True,
|
||||
transform=None,
|
||||
):
|
||||
name = "reddit"
|
||||
url = "https://data.dgl.ai/dataset/reddit.zip"
|
||||
self.self_loop = self_loop
|
||||
super().__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
# Expect two files extracted: reddit_data.npz & reddit_graph.npz
|
||||
data = np.load(os.path.join(self.raw_path, "reddit_data.npz"))
|
||||
feat = data["feature"] # shape [N, 602]
|
||||
labels = data["label"] # shape [N]
|
||||
split = data["node_types"] # 1=train,2=val,3=test
|
||||
|
||||
# Load adjacency
|
||||
adj = sp.load_npz(os.path.join(self.raw_path, "reddit_graph.npz"))
|
||||
src, dst = adj.nonzero()
|
||||
if self.self_loop:
|
||||
self_loops = np.arange(adj.shape[0])
|
||||
src = np.concatenate([src, self_loops])
|
||||
dst = np.concatenate([dst, self_loops])
|
||||
edges = list(zip(src, dst))
|
||||
|
||||
# Build graph
|
||||
g = eg.Graph()
|
||||
g.add_edges_from(edges)
|
||||
|
||||
# Assign node features, labels, and masks
|
||||
for i in range(feat.shape[0]):
|
||||
g.add_node(
|
||||
i,
|
||||
feat=feat[i],
|
||||
label=int(labels[i]),
|
||||
train_mask=(split[i] == 1),
|
||||
val_mask=(split[i] == 2),
|
||||
test_mask=(split[i] == 3),
|
||||
)
|
||||
|
||||
self._g = g
|
||||
self._num_classes = int(np.max(labels) + 1)
|
||||
|
||||
if self.verbose:
|
||||
print("Loaded Reddit dataset:")
|
||||
print(f" NumNodes: {g.number_of_nodes()}")
|
||||
print(f" NumEdges: {g.number_of_edges()}")
|
||||
print(f" NumFeats: {feat.shape[1]}")
|
||||
print(f" NumClasses: {self._num_classes}")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "RedditDataset only contains one graph"
|
||||
return self._g if self.transform is None else self.transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
return self._num_classes
|
||||
@@ -0,0 +1,107 @@
|
||||
"""RoadNet-CA Dataset
|
||||
|
||||
This dataset represents the road network of California.
|
||||
Nodes correspond to intersections, and edges represent roads connecting them.
|
||||
|
||||
The data is undirected and unweighted. No features or labels are provided.
|
||||
|
||||
Statistics:
|
||||
- Nodes: 1,965,206
|
||||
- Edges: 2,766,607
|
||||
- Features: None
|
||||
- Labels: None
|
||||
|
||||
Reference:
|
||||
J. Leskovec and A. Krevl, “SNAP Datasets: Stanford Large Network Dataset Collection,”
|
||||
https://snap.stanford.edu/data/roadNet-CA.html
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import download
|
||||
|
||||
|
||||
class RoadNetCADataset(EasyGraphBuiltinDataset):
|
||||
r"""Road network of California (RoadNet-CA)
|
||||
|
||||
Nodes are road intersections and edges are roads connecting them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str, optional
|
||||
Directory to store the raw downloaded files. Default: None
|
||||
force_reload : bool, optional
|
||||
Whether to re-download and process the dataset. Default: False
|
||||
verbose : bool, optional
|
||||
Whether to print detailed processing logs. Default: True
|
||||
transform : callable, optional
|
||||
Optional transform to apply on the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.datasets import RoadNetCADataset
|
||||
>>> dataset = RoadNetCADataset()
|
||||
>>> g = dataset[0]
|
||||
>>> print("Nodes:", g.number_of_nodes())
|
||||
>>> print("Edges:", g.number_of_edges())
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
name = "roadNet-CA"
|
||||
url = "https://snap.stanford.edu/data/roadNet-CA.txt.gz"
|
||||
super(RoadNetCADataset, self).__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def download(self):
|
||||
r"""Download and decompress the .txt.gz file."""
|
||||
compressed_path = os.path.join(self.raw_dir, self.name + ".txt.gz")
|
||||
extracted_path = os.path.join(self.raw_path, self.name + ".txt")
|
||||
|
||||
download(self.url, path=compressed_path)
|
||||
|
||||
if not os.path.exists(self.raw_path):
|
||||
os.makedirs(self.raw_path)
|
||||
|
||||
with gzip.open(compressed_path, "rb") as f_in:
|
||||
with open(extracted_path, "wb") as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
def process(self):
|
||||
graph = eg.Graph() # Undirected road network
|
||||
edge_list_path = os.path.join(self.raw_path, self.name + ".txt")
|
||||
|
||||
with open(edge_list_path, "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
continue
|
||||
u, v = map(int, line.strip().split())
|
||||
graph.add_edge(u, v)
|
||||
|
||||
self._g = graph
|
||||
self._num_nodes = graph.number_of_nodes()
|
||||
self._num_edges = graph.number_of_edges()
|
||||
|
||||
if self.verbose:
|
||||
print("Finished loading RoadNet-CA dataset.")
|
||||
print(f" NumNodes: {self._num_nodes}")
|
||||
print(f" NumEdges: {self._num_edges}")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "RoadNetCADataset only contains one graph"
|
||||
return self._g if self._transform is None else self._transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
import gzip
|
||||
import os
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.datasets.graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from easygraph.datasets.utils import download
|
||||
from easygraph.datasets.utils import extract_archive
|
||||
|
||||
|
||||
class TwitterEgoDataset(EasyGraphBuiltinDataset):
|
||||
r"""
|
||||
Twitter Ego Network Dataset
|
||||
|
||||
The Twitter dataset was collected from public sources and contains a large ego-network of Twitter users.
|
||||
The combined network includes 81K edges among 81K users.
|
||||
|
||||
Source: J. McAuley and J. Leskovec, Stanford SNAP, 2012
|
||||
URL: https://snap.stanford.edu/data/egonets-Twitter.html
|
||||
File used: https://snap.stanford.edu/data/twitter_combined.txt.gz
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(TwitterEgoDataset, self).__init__(
|
||||
name="twitter_ego",
|
||||
url="https://snap.stanford.edu/data/twitter_combined.txt.gz",
|
||||
force_reload=False,
|
||||
)
|
||||
|
||||
def download(self):
|
||||
gz_path = os.path.join(self.raw_path, "twitter_combined.txt.gz")
|
||||
download(self.url, path=gz_path)
|
||||
extract_archive(gz_path, self.raw_path)
|
||||
|
||||
def process(self):
|
||||
import gzip
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
gz_path = os.path.join(self.raw_path, "twitter_combined.txt.gz")
|
||||
txt_path = os.path.join(self.raw_path, "twitter_combined.txt")
|
||||
|
||||
if not os.path.exists(txt_path):
|
||||
with gzip.open(gz_path, "rt") as f_in, open(txt_path, "w") as f_out:
|
||||
f_out.writelines(f_in)
|
||||
|
||||
G = eg.Graph()
|
||||
edge_count = 0
|
||||
with open(txt_path, "r") as f:
|
||||
for line in f:
|
||||
u, v = map(int, line.strip().split())
|
||||
G.add_edge(u, v)
|
||||
edge_count += 1
|
||||
|
||||
self._graphs = [G]
|
||||
self._graph = G
|
||||
self._processed = True
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if self._graph is not None:
|
||||
return self._graph
|
||||
elif self._graphs:
|
||||
return self._graphs[idx]
|
||||
else:
|
||||
return None
|
||||
@@ -0,0 +1,358 @@
|
||||
import errno
|
||||
import hashlib
|
||||
import numbers
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
import torch as th
|
||||
|
||||
|
||||
__all__ = [
|
||||
"download",
|
||||
"extract_archive",
|
||||
"get_download_dir",
|
||||
"makedirs",
|
||||
"generate_mask_tensor",
|
||||
]
|
||||
|
||||
import warnings
|
||||
|
||||
from easygraph.utils.download import _retry
|
||||
|
||||
|
||||
def _get_eg_url(file_url):
|
||||
"""Get EasyGraph online url for download."""
|
||||
eg_repo_url = "https://gitlab.com/easy-graph/"
|
||||
repo_url = eg_repo_url
|
||||
if repo_url[-1] != "/":
|
||||
repo_url = repo_url + "/"
|
||||
return repo_url + file_url
|
||||
|
||||
|
||||
def _get_dgl_url(file_url):
|
||||
"""Get DGL online url for download."""
|
||||
dgl_repo_url = "https://data.dgl.ai/"
|
||||
repo_url = os.environ.get("DGL_REPO", dgl_repo_url)
|
||||
if repo_url[-1] != "/":
|
||||
repo_url = repo_url + "/"
|
||||
return repo_url + file_url
|
||||
|
||||
|
||||
def _set_labels(G, labels):
|
||||
index = 0
|
||||
for node in G.nodes:
|
||||
G.add_node(node, label=labels[index])
|
||||
index += 1
|
||||
return G
|
||||
|
||||
|
||||
def _set_features(G, features):
|
||||
index = 0
|
||||
for node in G.nodes:
|
||||
G.add_node(node, feat=features[index])
|
||||
index += 1
|
||||
return G
|
||||
|
||||
|
||||
def nonzero_1d(input):
|
||||
x = th.nonzero(input, as_tuple=False).squeeze()
|
||||
return x if x.dim() == 1 else x.view(-1)
|
||||
|
||||
|
||||
def tensor(data, dtype=None):
|
||||
if isinstance(data, numbers.Number):
|
||||
data = [data]
|
||||
if isinstance(data, list) and len(data) > 0 and isinstance(data[0], th.Tensor):
|
||||
# prevent GPU->CPU->GPU copies
|
||||
if data[0].ndim == 0:
|
||||
# zero dimension scalar tensors
|
||||
return th.stack(data)
|
||||
if isinstance(data, th.Tensor):
|
||||
return th.as_tensor(data, dtype=dtype, device=data.device)
|
||||
else:
|
||||
return th.as_tensor(data, dtype=dtype)
|
||||
|
||||
|
||||
def data_type_dict():
|
||||
return {
|
||||
"float16": th.float16,
|
||||
"float32": th.float32,
|
||||
"float64": th.float64,
|
||||
"uint8": th.uint8,
|
||||
"int8": th.int8,
|
||||
"int16": th.int16,
|
||||
"int32": th.int32,
|
||||
"int64": th.int64,
|
||||
"bool": th.bool,
|
||||
}
|
||||
|
||||
|
||||
def download(
|
||||
url,
|
||||
path=None,
|
||||
overwrite=True,
|
||||
sha1_hash=None,
|
||||
retries=5,
|
||||
verify_ssl=True,
|
||||
log=True,
|
||||
):
|
||||
"""Download a given URL.
|
||||
|
||||
Codes borrowed from mxnet/gluon/utils.py
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
URL to download.
|
||||
path : str, optional
|
||||
Destination path to store downloaded file. By default stores to the
|
||||
current directory with the same name as in url.
|
||||
overwrite : bool, optional
|
||||
Whether to overwrite the destination file if it already exists.
|
||||
By default always overwrites the downloaded file.
|
||||
sha1_hash : str, optional
|
||||
Expected sha1 hash in hexadecimal digits. Will ignore existing file when hash is specified
|
||||
but doesn't match.
|
||||
retries : integer, default 5
|
||||
The number of times to attempt downloading in case of failure or non 200 return codes.
|
||||
verify_ssl : bool, default True
|
||||
Verify SSL certificates.
|
||||
log : bool, default True
|
||||
Whether to print the progress for download
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The file path of the downloaded file.
|
||||
"""
|
||||
if path is None:
|
||||
fname = url.split("/")[-1]
|
||||
# Empty filenames are invalid
|
||||
assert fname, (
|
||||
"Can't construct file-name from this URL. "
|
||||
"Please set the `path` option manually."
|
||||
)
|
||||
else:
|
||||
path = os.path.expanduser(path)
|
||||
if os.path.isdir(path):
|
||||
fname = os.path.join(path, url.split("/")[-1])
|
||||
else:
|
||||
fname = path
|
||||
assert retries >= 0, "Number of retries should be at least 0"
|
||||
|
||||
if not verify_ssl:
|
||||
warnings.warn(
|
||||
"Unverified HTTPS request is being made (verify_ssl=False). "
|
||||
"Adding certificate verification is strongly advised."
|
||||
)
|
||||
|
||||
if (
|
||||
overwrite
|
||||
or not os.path.exists(fname)
|
||||
or (sha1_hash and not check_sha1(fname, sha1_hash))
|
||||
):
|
||||
dirname = os.path.dirname(os.path.abspath(os.path.expanduser(fname)))
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
while retries + 1 > 0:
|
||||
# Disable pyling too broad Exception
|
||||
# pylint: disable=W0703
|
||||
try:
|
||||
if log:
|
||||
print("Downloading %s from %s..." % (fname, url))
|
||||
r = requests.get(url, stream=True, verify=verify_ssl)
|
||||
if r.status_code != 200:
|
||||
raise RuntimeError("Failed downloading url %s" % url)
|
||||
with open(fname, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
f.write(chunk)
|
||||
if sha1_hash and not check_sha1(fname, sha1_hash):
|
||||
raise UserWarning(
|
||||
"File {} is downloaded but the content hash does not match."
|
||||
" The repo may be outdated or download may be incomplete. "
|
||||
'If the "repo_url" is overridden, consider switching to '
|
||||
"the default repo.".format(fname)
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
retries -= 1
|
||||
if retries <= 0:
|
||||
raise e
|
||||
else:
|
||||
if log:
|
||||
print(
|
||||
"download failed, retrying, {} attempt{} left".format(
|
||||
retries, "s" if retries > 1 else ""
|
||||
)
|
||||
)
|
||||
|
||||
return fname
|
||||
|
||||
|
||||
def extract_archive(file, target_dir, overwrite=False):
|
||||
"""Extract archive file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file : str
|
||||
Absolute path of the archive file.
|
||||
target_dir : str
|
||||
Target directory of the archive to be uncompressed.
|
||||
overwrite : bool, default True
|
||||
Whether to overwrite the contents inside the directory.
|
||||
By default always overwrites.
|
||||
"""
|
||||
if os.path.exists(target_dir) and not overwrite:
|
||||
return
|
||||
print("Extracting file to {}".format(target_dir))
|
||||
if file.endswith(".tar.gz") or file.endswith(".tar") or file.endswith(".tgz"):
|
||||
import tarfile
|
||||
|
||||
with tarfile.open(file, "r") as archive:
|
||||
archive.extractall(path=target_dir)
|
||||
elif file.endswith(".gz"):
|
||||
import gzip
|
||||
import shutil
|
||||
|
||||
with gzip.open(file, "rb") as f_in:
|
||||
target_file = os.path.join(target_dir, os.path.basename(file)[:-3])
|
||||
with open(target_file, "wb") as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
elif file.endswith(".zip"):
|
||||
import zipfile
|
||||
|
||||
with zipfile.ZipFile(file, "r") as archive:
|
||||
archive.extractall(path=target_dir)
|
||||
else:
|
||||
raise Exception("Unrecognized file type: " + file)
|
||||
|
||||
|
||||
def get_download_dir():
|
||||
"""Get the absolute path to the download directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dirname : str
|
||||
Path to the download directory
|
||||
"""
|
||||
default_dir = os.path.join(os.path.expanduser("~"), ".EasyGraphData")
|
||||
dirname = os.environ.get("EG_DOWNLOAD_DIR", default_dir)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
return dirname
|
||||
|
||||
|
||||
def makedirs(path):
|
||||
try:
|
||||
os.makedirs(os.path.expanduser(os.path.normpath(path)))
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST and os.path.isdir(path):
|
||||
raise e
|
||||
|
||||
|
||||
def check_sha1(filename, sha1_hash):
|
||||
"""Check whether the sha1 hash of the file content matches the expected hash.
|
||||
|
||||
Codes borrowed from mxnet/gluon/utils.py
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : str
|
||||
Path to the file.
|
||||
sha1_hash : str
|
||||
Expected sha1 hash in hexadecimal digits.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the file content matches the expected hash.
|
||||
"""
|
||||
sha1 = hashlib.sha1()
|
||||
with open(filename, "rb") as f:
|
||||
while True:
|
||||
data = f.read(1048576)
|
||||
if not data:
|
||||
break
|
||||
sha1.update(data)
|
||||
|
||||
return sha1.hexdigest() == sha1_hash
|
||||
|
||||
|
||||
def generate_mask_tensor(mask):
|
||||
"""Generate mask tensor according to different backend
|
||||
For torch, it will create a bool tensor
|
||||
Parameters
|
||||
----------
|
||||
mask: numpy ndarray
|
||||
input mask tensor
|
||||
"""
|
||||
assert isinstance(
|
||||
mask, np.ndarray
|
||||
), "input for generate_mask_tensor should be an numpy ndarray"
|
||||
return tensor(mask, dtype=data_type_dict()["bool"])
|
||||
|
||||
|
||||
def deprecate_property(old, new):
|
||||
warnings.warn(
|
||||
"Property {} will be deprecated, please use {} instead.".format(old, new)
|
||||
)
|
||||
|
||||
|
||||
def check_file(file_path: Path, md5: str):
|
||||
r"""Check if a file is valid.
|
||||
|
||||
Args:
|
||||
``file_path`` (``Path``): The local path of the file.
|
||||
``md5`` (``str``): The md5 of the file.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: Not found the file.
|
||||
"""
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"{file_path} does not exist.")
|
||||
else:
|
||||
with open(file_path, "rb") as f:
|
||||
data = f.read()
|
||||
cur_md5 = hashlib.md5(data).hexdigest()
|
||||
return cur_md5 == md5
|
||||
|
||||
|
||||
def download_file(url: str, file_path: Path):
|
||||
r"""Download a file from a url.
|
||||
|
||||
Args:
|
||||
``url`` (``str``): the url of the file
|
||||
``file_path`` (``str``): the path to the file
|
||||
"""
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
r = requests.get(url, stream=True, verify=True)
|
||||
if r.status_code != 200:
|
||||
raise requests.HTTPError(f"{url} is not accessible.")
|
||||
with open(file_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
@_retry(3)
|
||||
def download_and_check(url: str, file_path: Path, md5: str):
|
||||
r"""Download a file from a url and check its integrity.
|
||||
|
||||
Args:
|
||||
``url`` (``str``): The url of the file.
|
||||
``file_path`` (``Path``): The path to the file.
|
||||
``md5`` (``str``): The md5 of the file.
|
||||
"""
|
||||
if not file_path.exists():
|
||||
download_file(url, file_path)
|
||||
if not check_file(file_path, md5):
|
||||
file_path.unlink()
|
||||
raise ValueError(
|
||||
f"{file_path} is corrupted. We will delete it, and try to download it"
|
||||
" again."
|
||||
)
|
||||
return True
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Web-Google Dataset
|
||||
|
||||
This dataset is a web graph based on Google's web pages and their hyperlink
|
||||
structure, as crawled by the Stanford WebBase project in 2002.
|
||||
|
||||
Each node represents a web page, and a directed edge from u to v indicates
|
||||
a hyperlink from page u to page v.
|
||||
|
||||
Statistics:
|
||||
- Nodes: 875713
|
||||
- Edges: 5105039
|
||||
- Features: None
|
||||
- Labels: None
|
||||
|
||||
Reference:
|
||||
J. Leskovec, A. Rajaraman, J. Ullman, “Mining of Massive Datasets.”
|
||||
Dataset from SNAP: https://snap.stanford.edu/data/web-Google.html
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
from .graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from .utils import download
|
||||
from .utils import extract_archive
|
||||
|
||||
|
||||
class WebGoogleDataset(EasyGraphBuiltinDataset):
|
||||
r"""Web-Google hyperlink network dataset.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_dir : str, optional
|
||||
Directory to store the raw downloaded files. Default: None
|
||||
force_reload : bool, optional
|
||||
Whether to re-download and process the dataset. Default: False
|
||||
verbose : bool, optional
|
||||
Whether to print detailed processing logs. Default: True
|
||||
transform : callable, optional
|
||||
Optional transform to apply on the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.datasets import WebGoogleDataset
|
||||
>>> dataset = WebGoogleDataset()
|
||||
>>> g = dataset[0]
|
||||
>>> print("Nodes:", g.number_of_nodes())
|
||||
>>> print("Edges:", g.number_of_edges())
|
||||
"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
name = "web-Google"
|
||||
url = "https://snap.stanford.edu/data/web-Google.txt.gz"
|
||||
super(WebGoogleDataset, self).__init__(
|
||||
name=name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def download(self):
|
||||
r"""Download and extract .gz edge list."""
|
||||
if self.url is not None:
|
||||
file_path = os.path.join(self.raw_dir, self.name + ".txt.gz")
|
||||
download(self.url, path=file_path)
|
||||
extract_archive(file_path, self.raw_path)
|
||||
|
||||
def process(self):
|
||||
graph = eg.DiGraph() # Web-Google is directed
|
||||
edge_list_path = os.path.join(self.raw_path, self.name + ".txt")
|
||||
|
||||
with open(edge_list_path, "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
continue
|
||||
u, v = map(int, line.strip().split())
|
||||
graph.add_edge(u, v)
|
||||
|
||||
self._g = graph
|
||||
self._num_nodes = graph.number_of_nodes()
|
||||
self._num_edges = graph.number_of_edges()
|
||||
|
||||
if self.verbose:
|
||||
print("Finished loading Web-Google dataset.")
|
||||
print(f" NumNodes: {self._num_nodes}")
|
||||
print(f" NumEdges: {self._num_edges}")
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "WebGoogleDataset only contains one graph"
|
||||
return self._g if self._transform is None else self._transform(self._g)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
def download(self):
|
||||
r"""Download and decompress the .txt.gz file."""
|
||||
if self.url is not None:
|
||||
compressed_path = os.path.join(self.raw_dir, self.name + ".txt.gz")
|
||||
extracted_path = os.path.join(self.raw_path, self.name + ".txt")
|
||||
|
||||
# Download .gz file
|
||||
download(self.url, path=compressed_path)
|
||||
|
||||
# Ensure output directory exists
|
||||
if not os.path.exists(self.raw_path):
|
||||
os.makedirs(self.raw_path)
|
||||
|
||||
# Decompress manually
|
||||
with gzip.open(compressed_path, "rb") as f_in:
|
||||
with open(extracted_path, "wb") as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Wikipedia Top Categories Dataset (wiki-topcats)
|
||||
|
||||
This dataset is a directed graph of Wikipedia articles restricted to
|
||||
top-level categories (at least 100 articles), capturing the largest
|
||||
strongly connected component.
|
||||
|
||||
Statistics:
|
||||
- Nodes: 1,791,489
|
||||
- Edges: 28,511,807
|
||||
- Categories: 17,364
|
||||
- Overlapping labels per node
|
||||
|
||||
Source:
|
||||
H. Yin, A. Benson, J. Leskovec, D. Gleich.
|
||||
"Local Higher-order Graph Clustering", KDD 2017
|
||||
Data: https://snap.stanford.edu/data/wiki-topcats.html
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.datasets.graph_dataset_base import EasyGraphBuiltinDataset
|
||||
from easygraph.datasets.utils import download
|
||||
from easygraph.datasets.utils import extract_archive
|
||||
|
||||
|
||||
class WikiTopCatsDataset(EasyGraphBuiltinDataset):
|
||||
"""Wikipedia Top Categories Snapshot from 2011 (SNAP)"""
|
||||
|
||||
def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None):
|
||||
super(WikiTopCatsDataset, self).__init__(
|
||||
name="wiki_topcats",
|
||||
url="https://snap.stanford.edu/data/wiki-topcats.txt.gz",
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
def download(self):
|
||||
# Download the main graph file
|
||||
gz_path = os.path.join(self.raw_dir, "wiki-topcats.txt.gz")
|
||||
download(self.url, path=gz_path)
|
||||
|
||||
# Also download category info and page names
|
||||
cat_url = "https://snap.stanford.edu/data/wiki-topcats-categories.txt.gz"
|
||||
names_url = "https://snap.stanford.edu/data/wiki-topcats-page-names.txt.gz"
|
||||
download(
|
||||
cat_url, path=os.path.join(self.raw_dir, "wiki-topcats-categories.txt.gz")
|
||||
)
|
||||
download(
|
||||
names_url, path=os.path.join(self.raw_dir, "wiki-topcats-page-names.txt.gz")
|
||||
)
|
||||
|
||||
def process(self):
|
||||
raw = self.raw_dir
|
||||
|
||||
# Decompress and read edges
|
||||
edge_gz = os.path.join(raw, "wiki-topcats.txt.gz")
|
||||
edge_txt = os.path.join(raw, "wiki-topcats.txt")
|
||||
if not os.path.exists(edge_txt):
|
||||
with gzip.open(edge_gz, "rt") as fin, open(edge_txt, "w") as fout:
|
||||
fout.writelines(fin)
|
||||
G = eg.DiGraph()
|
||||
edge_count = 0
|
||||
with open(edge_txt, "r") as f:
|
||||
for line in f:
|
||||
u, v = map(int, line.strip().split())
|
||||
G.add_edge(u, v)
|
||||
edge_count += 1
|
||||
if self.verbose:
|
||||
print(f"Loaded graph: {G.number_of_nodes()} nodes, {edge_count} edges")
|
||||
|
||||
# Compress node names
|
||||
names_gz = os.path.join(raw, "wiki-topcats-page-names.txt.gz")
|
||||
names = {}
|
||||
with gzip.open(names_gz, "rt") as f:
|
||||
for idx, line in enumerate(f):
|
||||
names[idx] = line.strip()
|
||||
|
||||
# Load categories
|
||||
cats_gz = os.path.join(raw, "wiki-topcats-categories.txt.gz")
|
||||
labels = {} # mapping: node -> list of category strings
|
||||
with gzip.open(cats_gz, "rt") as f:
|
||||
for idx, line in enumerate(f):
|
||||
categories = line.strip().split(";")
|
||||
categories = [cat.strip() for cat in categories if cat.strip()]
|
||||
labels[idx] = categories
|
||||
|
||||
# Attach node features: empty, and node labels
|
||||
for n in G.nodes:
|
||||
G.add_node(n, name=names.get(n, ""), label=labels.get(n, []))
|
||||
|
||||
self._graph = G
|
||||
self._graphs = [G]
|
||||
self._processed = True
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0
|
||||
return self._graph
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
**********
|
||||
Exceptions
|
||||
**********
|
||||
|
||||
Base exceptions and errors for EasyGraph.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"HasACycle",
|
||||
"NodeNotFound",
|
||||
"EasyGraphAlgorithmError",
|
||||
"EasyGraphException",
|
||||
"EasyGraphError",
|
||||
"EasyGraphNoCycle",
|
||||
"EasyGraphNoPath",
|
||||
"EasyGraphNotImplemented",
|
||||
"EasyGraphPointlessConcept",
|
||||
"EasyGraphUnbounded",
|
||||
"EasyGraphUnfeasible",
|
||||
]
|
||||
|
||||
|
||||
class EasyGraphException(Exception):
|
||||
"""Base class for exceptions in EasyGraph."""
|
||||
|
||||
|
||||
class EasyGraphError(EasyGraphException):
|
||||
"""Exception for a serious error in EasyGraph"""
|
||||
|
||||
|
||||
class EasyGraphPointlessConcept(EasyGraphException):
|
||||
"""Raised when a null graph is provided as input to an algorithm
|
||||
that cannot use it.
|
||||
|
||||
The null graph is sometimes considered a pointless concept [1]_,
|
||||
thus the name of the exception.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Harary, F. and Read, R. "Is the Null Graph a Pointless
|
||||
Concept?" In Graphs and Combinatorics Conference, George
|
||||
Washington University. New York: Springer-Verlag, 1973.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class EasyGraphAlgorithmError(EasyGraphException):
|
||||
"""Exception for unexpected termination of algorithms."""
|
||||
|
||||
|
||||
class EasyGraphUnfeasible(EasyGraphAlgorithmError):
|
||||
"""Exception raised by algorithms trying to solve a problem
|
||||
instance that has no feasible solution."""
|
||||
|
||||
|
||||
class EasyGraphNoPath(EasyGraphUnfeasible):
|
||||
"""Exception for algorithms that should return a path when running
|
||||
on graphs where such a path does not exist."""
|
||||
|
||||
|
||||
class EasyGraphNoCycle(EasyGraphUnfeasible):
|
||||
"""Exception for algorithms that should return a cycle when running
|
||||
on graphs where such a cycle does not exist."""
|
||||
|
||||
|
||||
class HasACycle(EasyGraphException):
|
||||
"""Raised if a graph has a cycle when an algorithm expects that it
|
||||
will have no cycles.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class EasyGraphUnbounded(EasyGraphAlgorithmError):
|
||||
"""Exception raised by algorithms trying to solve a maximization
|
||||
or a minimization problem instance that is unbounded."""
|
||||
|
||||
|
||||
class EasyGraphNotImplemented(EasyGraphException):
|
||||
"""Exception raised by algorithms not implemented for a type of graph."""
|
||||
|
||||
|
||||
class NodeNotFound(EasyGraphException):
|
||||
"""Exception raised if requested node is not present in the graph"""
|
||||
@@ -0,0 +1,10 @@
|
||||
try:
|
||||
from .base import BaseTask
|
||||
from .hypergraphs import HypergraphVertexClassificationTask
|
||||
|
||||
|
||||
except:
|
||||
print(
|
||||
"Warning raise in module: experiments. Please install Pytorch before you use"
|
||||
" functions related to nueral network"
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
import abc
|
||||
import logging
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
import optuna
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from easygraph.classes.base import load_structure
|
||||
from easygraph.ml_metrics import BaseEvaluator
|
||||
from easygraph.utils import default_log_formatter
|
||||
from optuna.samplers import TPESampler
|
||||
|
||||
|
||||
class BaseTask:
|
||||
r"""The base class of Auto-experiment in EasyGraph.
|
||||
|
||||
Args:
|
||||
``work_root`` (``Optional[Union[str, Path]]``): User's work root to store all studies.
|
||||
``data`` (``dict``): The dictionary to store input data that used in the experiment.
|
||||
``model_builder`` (``Callable``): The function to build a model with a fixed parameter ``trial``.
|
||||
``train_builder`` (``Callable``): The function to build a training configuration with two fixed parameters ``trial`` and ``model``.
|
||||
``evaluator`` (``eg.ml_metrics.BaseEvaluator``): The EasyGraph evaluator object to evaluate performance of the model in the experiment.
|
||||
``device`` (``torch.device``): The target device to run the experiment.
|
||||
``structure_builder`` (``Optional[Callable]``): The function to build a structure with a fixed parameter ``trial``. The structure can be ``eg.Graph``, ``eg.DiGraph``, ``eg.BiGraph``, and ``eg.Hypergraph``.
|
||||
``study_name`` (``Optional[str]``): The name of this study. If set to ``None``, the study name will be generated automatically according to current time. Defaults to ``None``.
|
||||
``overwrite`` (``bool``): The flag that whether to overwrite the existing study. Different studies are identified by the ``study_name``. Defaults to ``True``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
work_root: Optional[Union[str, Path]],
|
||||
data: dict,
|
||||
model_builder: Callable,
|
||||
train_builder: Callable,
|
||||
evaluator: BaseEvaluator,
|
||||
device: torch.device,
|
||||
structure_builder: Optional[Callable] = None,
|
||||
study_name: Optional[str] = None,
|
||||
overwrite: bool = True,
|
||||
):
|
||||
self.data = data
|
||||
self.model_builder = model_builder
|
||||
self.train_builder = train_builder
|
||||
self.structure_builder = structure_builder
|
||||
self.evaluator = evaluator
|
||||
self.device = device
|
||||
self.study = None
|
||||
if study_name is None:
|
||||
self.study_name = time.strftime("%Y-%m-%d--%H-%M-%S", time.localtime())
|
||||
else:
|
||||
self.study_name = study_name
|
||||
work_root = Path(work_root)
|
||||
self.study_root = work_root / self.study_name
|
||||
if overwrite and self.study_root.exists():
|
||||
shutil.rmtree(self.study_root)
|
||||
self.log_file = self.study_root / "log.txt"
|
||||
self.cache_root = self.study_root / "cache"
|
||||
if not work_root.exists():
|
||||
if work_root.parent.exists():
|
||||
work_root.mkdir(exist_ok=True)
|
||||
else:
|
||||
raise ValueError(f"The work_root {work_root} does not exist.")
|
||||
self.study_root.mkdir(exist_ok=True)
|
||||
self.cache_root.mkdir(exist_ok=True)
|
||||
# configure logging
|
||||
self.logger = optuna.logging.get_logger("optuna")
|
||||
self.logger.setLevel(logging.INFO)
|
||||
out_file_handler = logging.FileHandler(self.log_file, mode="a", encoding="utf8")
|
||||
out_file_handler.setFormatter(default_log_formatter())
|
||||
self.logger.addHandler(out_file_handler)
|
||||
self.logger.info(f"Logs will be saved to {self.log_file.absolute()}")
|
||||
self.logger.info(
|
||||
f"Files in training will be saved in {self.study_root.absolute()}"
|
||||
)
|
||||
|
||||
def experiment(self, trial: optuna.Trial):
|
||||
r"""Run the experiment for a given trial.
|
||||
|
||||
Args:
|
||||
``trial`` (``optuna.Trial``): The ``optuna.Trial`` object.
|
||||
"""
|
||||
if self.structure_builder is not None:
|
||||
self.data["structure"] = self.structure_builder(trial).to(self.device)
|
||||
model = self.model_builder(trial).to(self.device)
|
||||
train_configs: dict = self.train_builder(trial, model)
|
||||
assert "optimizer" in train_configs.keys()
|
||||
optimizer = train_configs["optimizer"]
|
||||
assert "criterion" in train_configs.keys()
|
||||
criterion = train_configs["criterion"]
|
||||
scheduler = train_configs.get("scheduler", None)
|
||||
|
||||
best_model = None
|
||||
if self.direction == "maximize":
|
||||
best_score = -float("inf")
|
||||
else:
|
||||
best_score = float("inf")
|
||||
for epoch in range(self.max_epoch):
|
||||
self.train(self.data, model, optimizer, criterion)
|
||||
val_res = self.validate(self.data, model)
|
||||
trial.report(val_res, epoch)
|
||||
if trial.should_prune():
|
||||
raise optuna.exceptions.TrialPruned()
|
||||
if scheduler is not None:
|
||||
scheduler.step()
|
||||
if self.direction == "maximize":
|
||||
if val_res > best_score:
|
||||
best_score = val_res
|
||||
best_model = deepcopy(model)
|
||||
with open(self.cache_root / f"{trial.number}_model.pth", "wb") as f:
|
||||
torch.save(best_model.cpu().state_dict(), f)
|
||||
self.data["structure"].save(self.cache_root / f"{trial.number}_structure.dhg")
|
||||
return best_score
|
||||
|
||||
def _remove_cached_data(self):
|
||||
r"""Remove cached models and structures."""
|
||||
if self.study is not None:
|
||||
for filename in self.cache_root.glob("*"):
|
||||
if filename.stem.split("_")[0] != str(self.study.best_trial.number):
|
||||
filename.unlink()
|
||||
|
||||
def run(self, max_epoch: int, num_trials: int = 1, direction: str = "maximize"):
|
||||
r"""Run experiments with automatically hyper-parameter tuning.
|
||||
|
||||
Args:
|
||||
``max_epoch`` (``int``): The maximum number of epochs to train for each experiment.
|
||||
``num_trials`` (``int``): The number of trials to run. Defaults to ``1``.
|
||||
``direction`` (``str``): The direction to optimize. Defaults to ``"maximize"``.
|
||||
"""
|
||||
self.logger.info(f"Random seed is {dhg.random.seed()}")
|
||||
sampler = TPESampler(seed=dhg.random.seed())
|
||||
self.max_epoch, self.direction = max_epoch, direction
|
||||
self.study = optuna.create_study(direction=direction, sampler=sampler)
|
||||
self.study.optimize(self.experiment, n_trials=num_trials, timeout=600)
|
||||
|
||||
self._remove_cached_data()
|
||||
self.best_model = self.model_builder(self.study.best_trial)
|
||||
self.best_model.load_state_dict(
|
||||
torch.load(f"{self.cache_root}/{self.study.best_trial.number}_model.pth")
|
||||
)
|
||||
self.best_structure = load_structure(
|
||||
f"{self.cache_root}/{self.study.best_trial.number}_structure.dhg"
|
||||
)
|
||||
self.best_model = self.best_model.to(self.device)
|
||||
self.best_structure = self.best_structure.to(self.device)
|
||||
|
||||
self.logger.info("Best trial:")
|
||||
self.best_trial = self.study.best_trial
|
||||
self.logger.info(f"\tValue: {self.best_trial.value:.3f}")
|
||||
self.logger.info(f"\tParams:")
|
||||
for key, value in self.best_trial.params.items():
|
||||
self.logger.info(f"\t\t{key} |-> {value}")
|
||||
test_res = self.test()
|
||||
self.logger.info(f"Final test results:")
|
||||
for key, value in test_res.items():
|
||||
self.logger.info(f"\t{key} |-> {value:.3f}")
|
||||
|
||||
@abc.abstractmethod
|
||||
def train(
|
||||
self,
|
||||
data: dict,
|
||||
model: nn.Module,
|
||||
optimizer: torch.optim.Optimizer,
|
||||
criterion: nn.Module,
|
||||
):
|
||||
r"""Train model for one epoch.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``): The input data.
|
||||
``model`` (``nn.Module``): The model.
|
||||
``optimizer`` (``torch.optim.Optimizer``): The model optimizer.
|
||||
``criterion`` (``nn.Module``): The loss function.
|
||||
"""
|
||||
|
||||
@torch.no_grad()
|
||||
@abc.abstractmethod
|
||||
def validate(
|
||||
self,
|
||||
data: dict,
|
||||
model: nn.Module,
|
||||
):
|
||||
r"""Validate the model.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``): The input data.
|
||||
``model`` (``nn.Module``): The model.
|
||||
"""
|
||||
|
||||
@torch.no_grad()
|
||||
@abc.abstractmethod
|
||||
def test(self, data: Optional[dict] = None, model: Optional[nn.Module] = None):
|
||||
r"""Test the model.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``, optional): The input data if set to ``None``, the specified ``data`` in the initialization of the experiments will be used. Defaults to ``None``.
|
||||
``model`` (``nn.Module``, optional): The model if set to ``None``, the trained best model will be used. Defaults to ``None``.
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
from .hypergraph import HypergraphVertexClassificationTask
|
||||
@@ -0,0 +1,121 @@
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
import optuna
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from easygraph.ml_metrics import BaseEvaluator
|
||||
|
||||
from ..vertex_classification import VertexClassificationTask
|
||||
|
||||
|
||||
class HypergraphVertexClassificationTask(VertexClassificationTask):
|
||||
r"""The auto-experiment class for the vertex classification task on hypergraph.
|
||||
|
||||
Args:
|
||||
``work_root`` (``Optional[Union[str, Path]]``): User's work root to store all studies.
|
||||
``data`` (``dict``): The dictionary to store input data that used in the experiment.
|
||||
``model_builder`` (``Callable``): The function to build a model with a fixed parameter ``trial``.
|
||||
``train_builder`` (``Callable``): The function to build a training configuration with two fixed parameters ``trial`` and ``model``.
|
||||
``evaluator`` (``easygraph.ml_metrics.BaseEvaluator``): The DHG evaluator object to evaluate performance of the model in the experiment.
|
||||
``device`` (``torch.device``): The target device to run the experiment.
|
||||
``structure_builder`` (``Optional[Callable]``): The function to build a structure with a fixed parameter ``trial``. The structure should be ``easygraph.Hypergraph``.
|
||||
``study_name`` (``Optional[str]``): The name of this study. If set to ``None``, the study name will be generated automatically according to current time. Defaults to ``None``.
|
||||
``overwrite`` (``bool``): The flag that whether to overwrite the existing study. Different studies are identified by the ``study_name``. Defaults to ``True``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
work_root: Optional[Union[str, Path]],
|
||||
data: dict,
|
||||
model_builder: Callable,
|
||||
train_builder: Callable,
|
||||
evaluator: BaseEvaluator,
|
||||
device: torch.device,
|
||||
structure_builder: Optional[Callable] = None,
|
||||
study_name: Optional[str] = None,
|
||||
overwrite: bool = True,
|
||||
):
|
||||
super().__init__(
|
||||
work_root,
|
||||
data,
|
||||
model_builder,
|
||||
train_builder,
|
||||
evaluator,
|
||||
device,
|
||||
structure_builder=structure_builder,
|
||||
study_name=study_name,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
|
||||
def to(self, device: torch.device):
|
||||
r"""Move the input data to the target device.
|
||||
|
||||
Args:
|
||||
``device`` (``torch.device``): The specified target device to store the input data.
|
||||
"""
|
||||
return super().to(device)
|
||||
|
||||
@property
|
||||
def vars_for_DL(self):
|
||||
r"""Return a name list for available variables for deep learning in the vertex classification on hypergraph. The name list includes ``features``, ``structure``, ``labels``, ``train_mask``, ``val_mask``, and ``test_mask``.
|
||||
"""
|
||||
return super().vars_for_DL
|
||||
|
||||
def experiment(self, trial: optuna.Trial):
|
||||
r"""Run the experiment for a given trial.
|
||||
|
||||
Args:
|
||||
``trial`` (``optuna.Trial``): The ``optuna.Trial`` object.
|
||||
"""
|
||||
return super().experiment(trial)
|
||||
|
||||
def run(self, max_epoch: int, num_trials: int = 1, direction: str = "maximize"):
|
||||
r"""Run experiments with automatically hyper-parameter tuning.
|
||||
|
||||
Args:
|
||||
``max_epoch`` (``int``): The maximum number of epochs to train for each experiment.
|
||||
``num_trials`` (``int``): The number of trials to run. Defaults to ``1``.
|
||||
``direction`` (``str``): The direction to optimize. Defaults to ``"maximize"``.
|
||||
"""
|
||||
return super().run(max_epoch, num_trials, direction)
|
||||
|
||||
def train(
|
||||
self,
|
||||
data: dict,
|
||||
model: nn.Module,
|
||||
optimizer: torch.optim.Optimizer,
|
||||
criterion: nn.Module,
|
||||
):
|
||||
r"""Train model for one epoch.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``): The input data.
|
||||
``model`` (``nn.Module``): The model.
|
||||
``optimizer`` (``torch.optim.Optimizer``): The model optimizer.
|
||||
``criterion`` (``nn.Module``): The loss function.
|
||||
"""
|
||||
return super().train(data, model, optimizer, criterion)
|
||||
|
||||
@torch.no_grad()
|
||||
def validate(self, data: dict, model: nn.Module):
|
||||
r"""Validate the model.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``): The input data.
|
||||
``model`` (``nn.Module``): The model.
|
||||
"""
|
||||
return super().validate(data, model)
|
||||
|
||||
@torch.no_grad()
|
||||
def test(self, data: Optional[dict] = None, model: Optional[nn.Module] = None):
|
||||
r"""Test the model.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``, optional): The input data if set to ``None``, the specified ``data`` in the intialization of the experiments will be used. Defaults to ``None``.
|
||||
``model`` (``nn.Module``, optional): The model if set to ``None``, the trained best model will be used. Defaults to ``None``.
|
||||
"""
|
||||
return super().test(data, model)
|
||||
@@ -0,0 +1,166 @@
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
import optuna
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from easygraph.ml_metrics import BaseEvaluator
|
||||
|
||||
from .base import BaseTask
|
||||
|
||||
|
||||
class VertexClassificationTask(BaseTask):
|
||||
r"""The auto-experiment class for the vertex classification task.
|
||||
|
||||
Args:
|
||||
``work_root`` (``Optional[Union[str, Path]]``): User's work root to store all studies.
|
||||
``data`` (``dict``): The dictionary to store input data that used in the experiment.
|
||||
``model_builder`` (``Callable``): The function to build a model with a fixed parameter ``trial``.
|
||||
``train_builder`` (``Callable``): The function to build a training configuration with two fixed parameters ``trial`` and ``model``.
|
||||
``evaluator`` (``eg.ml_metrics.BaseEvaluator``): The DHG evaluator object to evaluate performance of the model in the experiment.
|
||||
``device`` (``torch.device``): The target device to run the experiment.
|
||||
``structure_builder`` (``Optional[Callable]``): The function to build a structure with a fixed parameter ``trial``. The structure can be ``eg.Hypergraph``.
|
||||
``study_name`` (``Optional[str]``): The name of this study. If set to ``None``, the study name will be generated automatically according to current time. Defaults to ``None``.
|
||||
``overwrite`` (``bool``): The flag that whether to overwrite the existing study. Different studies are identified by the ``study_name``. Defaults to ``True``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
work_root: Optional[Union[str, Path]],
|
||||
data: dict,
|
||||
model_builder: Callable,
|
||||
train_builder: Callable,
|
||||
evaluator: BaseEvaluator,
|
||||
device: torch.device,
|
||||
structure_builder: Optional[Callable] = None,
|
||||
study_name: Optional[str] = None,
|
||||
overwrite: bool = True,
|
||||
):
|
||||
super().__init__(
|
||||
work_root,
|
||||
data,
|
||||
model_builder,
|
||||
train_builder,
|
||||
evaluator,
|
||||
device,
|
||||
structure_builder=structure_builder,
|
||||
study_name=study_name,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
self.to(self.device)
|
||||
|
||||
def to(self, device: torch.device):
|
||||
r"""Move the input data to the target device.
|
||||
|
||||
Args:
|
||||
``device`` (``torch.device``): The specified target device to store the input data.
|
||||
"""
|
||||
self.device = device
|
||||
for name in self.vars_for_DL:
|
||||
if name in self.data.keys():
|
||||
self.data[name] = self.data[name].to(device)
|
||||
return self
|
||||
|
||||
@property
|
||||
def vars_for_DL(self):
|
||||
r"""Return a name list for available variables for deep learning in the vertex classification task. The name list includes ``features``, ``structure``, ``labels``, ``train_mask``, ``val_mask``, and ``test_mask``.
|
||||
"""
|
||||
return (
|
||||
"features",
|
||||
"structure",
|
||||
"labels",
|
||||
"train_mask",
|
||||
"val_mask",
|
||||
"test_mask",
|
||||
)
|
||||
|
||||
def experiment(self, trial: optuna.Trial):
|
||||
r"""Run the experiment for a given trial.
|
||||
|
||||
Args:
|
||||
``trial`` (``optuna.Trial``): The ``optuna.Trial`` object.
|
||||
"""
|
||||
return super().experiment(trial)
|
||||
|
||||
def run(self, max_epoch: int, num_trials: int = 1, direction: str = "maximize"):
|
||||
r"""Run experiments with automatically hyper-parameter tuning.
|
||||
|
||||
Args:
|
||||
``max_epoch`` (``int``): The maximum number of epochs to train for each experiment.
|
||||
``num_trials`` (``int``): The number of trials to run. Defaults to ``1``.
|
||||
``direction`` (``str``): The direction to optimize. Defaults to ``"maximize"``.
|
||||
"""
|
||||
return super().run(max_epoch, num_trials, direction)
|
||||
|
||||
def train(
|
||||
self,
|
||||
data: dict,
|
||||
model: nn.Module,
|
||||
optimizer: torch.optim.Optimizer,
|
||||
criterion: nn.Module,
|
||||
):
|
||||
r"""Train model for one epoch.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``): The input data.
|
||||
``model`` (``nn.Module``): The model.
|
||||
``optimizer`` (``torch.optim.Optimizer``): The model optimizer.
|
||||
``criterion`` (``nn.Module``): The loss function.
|
||||
"""
|
||||
features, structure = data["features"], data["structure"]
|
||||
train_mask, labels = data["train_mask"], data["labels"]
|
||||
model.train()
|
||||
optimizer.zero_grad()
|
||||
outputs = model(features, structure)
|
||||
loss = criterion(
|
||||
outputs[train_mask],
|
||||
labels[train_mask],
|
||||
)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
@torch.no_grad()
|
||||
def validate(self, data: dict, model: nn.Module):
|
||||
r"""Validate the model.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``): The input data.
|
||||
``model`` (``nn.Module``): The model.
|
||||
"""
|
||||
features, structure = data["features"], data["structure"]
|
||||
val_mask, labels = data["val_mask"], data["labels"]
|
||||
model.eval()
|
||||
outputs = model(features, structure)
|
||||
res = self.evaluator.validate(labels[val_mask], outputs[val_mask])
|
||||
return res
|
||||
|
||||
@torch.no_grad()
|
||||
def test(self, data: Optional[dict] = None, model: Optional[nn.Module] = None):
|
||||
r"""Test the model.
|
||||
|
||||
Args:
|
||||
``data`` (``dict``, optional): The input data if set to ``None``, the specified ``data`` in the initialization of the experiments will be used. Defaults to ``None``.
|
||||
``model`` (``nn.Module``, optional): The model if set to ``None``, the trained best model will be used. Defaults to ``None``.
|
||||
"""
|
||||
if data is None:
|
||||
features, structure = self.data["features"], self.best_structure
|
||||
test_mask, labels = self.data["test_mask"], self.data["labels"]
|
||||
else:
|
||||
features, structure = (
|
||||
data["features"].to(self.device),
|
||||
data["structure"].to(self.device),
|
||||
)
|
||||
test_mask, labels = (
|
||||
data["test_mask"].to(self.device),
|
||||
data["labels"].to(self.device),
|
||||
)
|
||||
if model is None:
|
||||
model = self.best_model
|
||||
model = model.to(self.device)
|
||||
model.eval()
|
||||
outputs = model(features, structure)
|
||||
res = self.evaluator.test(labels[test_mask], outputs[test_mask])
|
||||
return res
|
||||
@@ -0,0 +1,21 @@
|
||||
from easygraph.functions.basic import *
|
||||
from easygraph.functions.centrality import *
|
||||
from easygraph.functions.community import *
|
||||
from easygraph.functions.components import *
|
||||
from easygraph.functions.core import *
|
||||
from easygraph.functions.drawing import *
|
||||
from easygraph.functions.graph_embedding import *
|
||||
from easygraph.functions.graph_generator import *
|
||||
from easygraph.functions.isolate import *
|
||||
from easygraph.functions.path import *
|
||||
from easygraph.functions.structural_holes import *
|
||||
|
||||
|
||||
try:
|
||||
from easygraph.functions.hypergraph import *
|
||||
except:
|
||||
print(
|
||||
"Warning raise in module:model.Please install "
|
||||
"Pytorch before you use functions"
|
||||
" related to Hypergraph"
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .avg_degree import *
|
||||
from .cluster import *
|
||||
from .localassort import *
|
||||
from .predecessor_path_based import *
|
||||
@@ -0,0 +1,31 @@
|
||||
__all__ = [
|
||||
"average_degree",
|
||||
]
|
||||
|
||||
|
||||
def average_degree(G) -> float:
|
||||
"""Returns the average degree of the graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
average degree : float
|
||||
The average degree of the graph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Self loops are counted twice in the total degree of a node.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G.add_edge(1, 2)
|
||||
>>> G.add_edge(2, 3)
|
||||
>>> eg.average_degree(G)
|
||||
1.3333333333333333
|
||||
"""
|
||||
return G.number_of_edges() / G.number_of_nodes() * 2
|
||||
@@ -0,0 +1,559 @@
|
||||
from collections import Counter
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils.decorators import hybrid
|
||||
from easygraph.utils.decorators import not_implemented_for
|
||||
from easygraph.utils.misc import split
|
||||
from easygraph.utils.misc import split_len
|
||||
|
||||
|
||||
__all__ = ["average_clustering", "clustering"]
|
||||
|
||||
|
||||
def _local_weighted_triangles_and_degree_iter_parallel(
|
||||
nodes_nbrs, G, weight, max_weight
|
||||
):
|
||||
ret = []
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
for i, nbrs in nodes_nbrs:
|
||||
inbrs = set(nbrs) - {i}
|
||||
weighted_triangles = 0
|
||||
seen = set()
|
||||
for j in inbrs:
|
||||
seen.add(j)
|
||||
# This avoids counting twice -- we double at the end.
|
||||
jnbrs = set(G[j]) - seen
|
||||
# Only compute the edge weight once, before the inner inner
|
||||
# loop.
|
||||
wij = wt(i, j)
|
||||
weighted_triangles += sum(
|
||||
np.cbrt([(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs])
|
||||
)
|
||||
ret.append((i, len(inbrs), 2 * weighted_triangles))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _weighted_triangles_and_degree_iter(G, nodes=None, weight="weight", n_workers=None):
|
||||
"""Return an iterator of (node, degree, weighted_triangles).
|
||||
|
||||
Used for weighted clustering.
|
||||
Note: this returns the geometric average weight of edges in the triangle.
|
||||
Also, each triangle is counted twice (each direction).
|
||||
So you may want to divide by 2.
|
||||
|
||||
"""
|
||||
|
||||
if weight is None or G.number_of_edges() == 0:
|
||||
max_weight = 1
|
||||
else:
|
||||
max_weight = max(d.get(weight, 1) for u, v, d in G.edges)
|
||||
if nodes is None:
|
||||
nodes_nbrs = G.adj.items()
|
||||
else:
|
||||
nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_weighted_triangles_and_degree_iter_function = partial(
|
||||
_local_weighted_triangles_and_degree_iter_parallel,
|
||||
G=G,
|
||||
weight=weight,
|
||||
max_weight=max_weight,
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(_local_weighted_triangles_and_degree_iter_function, nodes_nbrs)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for i, nbrs in nodes_nbrs:
|
||||
inbrs = set(nbrs) - {i}
|
||||
weighted_triangles = 0
|
||||
seen = set()
|
||||
for j in inbrs:
|
||||
seen.add(j)
|
||||
# This avoids counting twice -- we double at the end.
|
||||
jnbrs = set(G[j]) - seen
|
||||
# Only compute the edge weight once, before the inner inner
|
||||
# loop.
|
||||
wij = wt(i, j)
|
||||
weighted_triangles += sum(
|
||||
np.cbrt([(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs])
|
||||
)
|
||||
yield (i, len(inbrs), 2 * weighted_triangles)
|
||||
|
||||
|
||||
def _local_directed_weighted_triangles_and_degree_parallel(
|
||||
nodes_nbrs, G, weight, max_weight
|
||||
):
|
||||
ret = []
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in ipreds:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
for j in isuccs:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
ret.append([i, dtotal, dbidirectional, directed_triangles])
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _directed_weighted_triangles_and_degree_iter(
|
||||
G, nodes=None, weight="weight", n_workers=None
|
||||
):
|
||||
"""Return an iterator of
|
||||
(node, total_degree, reciprocal_degree, directed_weighted_triangles).
|
||||
|
||||
Used for directed weighted clustering.
|
||||
Note that unlike `_weighted_triangles_and_degree_iter()`, this function counts
|
||||
directed triangles so does not count triangles twice.
|
||||
|
||||
"""
|
||||
|
||||
if weight is None or G.number_of_edges() == 0:
|
||||
max_weight = 1
|
||||
else:
|
||||
max_weight = max(d.get(weight, 1) for u, v, d in G.edges)
|
||||
|
||||
nodes_nbrs = ((n, G._pred[n], G._adj[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_directed_weighted_triangles_and_degree_function = partial(
|
||||
_local_directed_weighted_triangles_and_degree_parallel,
|
||||
G=G,
|
||||
weight=weight,
|
||||
max_weight=max_weight,
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(
|
||||
_local_directed_weighted_triangles_and_degree_function, nodes_nbrs
|
||||
)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
|
||||
else:
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in ipreds:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
for j in isuccs:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
yield (i, dtotal, dbidirectional, directed_triangles)
|
||||
|
||||
|
||||
def average_clustering(G, nodes=None, weight=None, count_zeros=True, n_workers=None):
|
||||
r"""Compute the average clustering coefficient for the graph G.
|
||||
|
||||
The clustering coefficient for the graph is the average,
|
||||
|
||||
.. math::
|
||||
|
||||
C = \frac{1}{n}\sum_{v \in G} c_v,
|
||||
|
||||
where :math:`n` is the number of nodes in `G`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
|
||||
nodes : container of nodes, optional (default=all nodes in G)
|
||||
Compute average clustering for nodes in this container.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
The edge attribute that holds the numerical value used as a weight.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
count_zeros : bool
|
||||
If False include only the nodes with nonzero clustering in the average.
|
||||
|
||||
Returns
|
||||
-------
|
||||
avg : float
|
||||
Average clustering
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.complete_graph(5)
|
||||
>>> print(eg.average_clustering(G))
|
||||
1.0
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a space saving routine; it might be faster
|
||||
to use the clustering function to get a list and then take the average.
|
||||
|
||||
Self loops are ignored.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Generalizations of the clustering coefficient to weighted
|
||||
complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
|
||||
K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
|
||||
http://jponnela.com/web_documents/a9.pdf
|
||||
.. [2] Marcus Kaiser, Mean clustering coefficients: the role of isolated
|
||||
nodes and leafs on clustering measures for small-world networks.
|
||||
https://arxiv.org/abs/0802.2512
|
||||
"""
|
||||
c = clustering(G, nodes, weight=weight, n_workers=n_workers).values()
|
||||
if not count_zeros:
|
||||
c = [v for v in c if abs(v) > 0]
|
||||
return sum(c) / len(c)
|
||||
|
||||
|
||||
def _local_directed_triangles_and_degree_iter_parallel(nodes_nbrs, G):
|
||||
ret = []
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in chain(ipreds, isuccs):
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
1
|
||||
for k in chain(
|
||||
(ipreds & jpreds),
|
||||
(ipreds & jsuccs),
|
||||
(isuccs & jpreds),
|
||||
(isuccs & jsuccs),
|
||||
)
|
||||
)
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
ret.append((i, dtotal, dbidirectional, directed_triangles))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _directed_triangles_and_degree_iter(G, nodes=None, n_workers=None):
|
||||
"""Return an iterator of
|
||||
(node, total_degree, reciprocal_degree, directed_triangles).
|
||||
|
||||
Used for directed clustering.
|
||||
Note that unlike `_triangles_and_degree_iter()`, this function counts
|
||||
directed triangles so does not count triangles twice.
|
||||
|
||||
"""
|
||||
nodes_nbrs = ((n, G._pred[n], G._adj[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_directed_triangles_and_degree_iter_parallel_function = partial(
|
||||
_local_directed_triangles_and_degree_iter_parallel, G=G
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes_nbrs, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(
|
||||
_local_directed_triangles_and_degree_iter_parallel_function, nodes_nbrs
|
||||
)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in chain(ipreds, isuccs):
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
1
|
||||
for k in chain(
|
||||
(ipreds & jpreds),
|
||||
(ipreds & jsuccs),
|
||||
(isuccs & jpreds),
|
||||
(isuccs & jsuccs),
|
||||
)
|
||||
)
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
yield (i, dtotal, dbidirectional, directed_triangles)
|
||||
|
||||
|
||||
def _local_triangles_and_degree_iter_function_parallel(nodes_nbrs, G):
|
||||
ret = []
|
||||
for v, v_nbrs in nodes_nbrs:
|
||||
vs = set(v_nbrs) - {v}
|
||||
gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs)
|
||||
ntriangles = sum(k * val for k, val in gen_degree.items())
|
||||
ret.append((v, len(vs), ntriangles, gen_degree))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _triangles_and_degree_iter(G, nodes=None, n_workers=None):
|
||||
"""Return an iterator of (node, degree, triangles, generalized degree).
|
||||
|
||||
This double counts triangles so you may want to divide by 2.
|
||||
See degree(), triangles() and generalized_degree() for definitions
|
||||
and details.
|
||||
|
||||
"""
|
||||
if nodes is None:
|
||||
nodes_nbrs = G.adj.items()
|
||||
else:
|
||||
nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_triangles_and_degree_iter_function = partial(
|
||||
_local_triangles_and_degree_iter_function_parallel, G=G
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes_nbrs, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(_local_triangles_and_degree_iter_function, nodes_nbrs)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for v, v_nbrs in nodes_nbrs:
|
||||
vs = set(v_nbrs) - {v}
|
||||
gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs)
|
||||
ntriangles = sum(k * val for k, val in gen_degree.items())
|
||||
yield (v, len(vs), ntriangles, gen_degree)
|
||||
|
||||
|
||||
@hybrid("cpp_clustering")
|
||||
def clustering(G, nodes=None, weight=None, n_workers=None):
|
||||
r"""Compute the clustering coefficient for nodes.
|
||||
|
||||
For unweighted graphs, the clustering of a node :math:`u`
|
||||
is the fraction of possible triangles through that node that exist,
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{2 T(u)}{deg(u)(deg(u)-1)},
|
||||
|
||||
where :math:`T(u)` is the number of triangles through node :math:`u` and
|
||||
:math:`deg(u)` is the degree of :math:`u`.
|
||||
|
||||
For weighted graphs, there are several ways to define clustering [1]_.
|
||||
the one used here is defined
|
||||
as the geometric average of the subgraph edge weights [2]_,
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{1}{deg(u)(deg(u)-1))}
|
||||
\sum_{vw} (\hat{w}_{uv} \hat{w}_{uw} \hat{w}_{vw})^{1/3}.
|
||||
|
||||
The edge weights :math:`\hat{w}_{uv}` are normalized by the maximum weight
|
||||
in the network :math:`\hat{w}_{uv} = w_{uv}/\max(w)`.
|
||||
|
||||
The value of :math:`c_u` is assigned to 0 if :math:`deg(u) < 2`.
|
||||
|
||||
Additionally, this weighted definition has been generalized to support negative edge weights [3]_.
|
||||
|
||||
For directed graphs, the clustering is similarly defined as the fraction
|
||||
of all possible directed triangles or geometric average of the subgraph
|
||||
edge weights for unweighted and weighted directed graph respectively [4]_.
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{2}{deg^{tot}(u)(deg^{tot}(u)-1) - 2deg^{\leftrightarrow}(u)}
|
||||
T(u),
|
||||
|
||||
where :math:`T(u)` is the number of directed triangles through node
|
||||
:math:`u`, :math:`deg^{tot}(u)` is the sum of in degree and out degree of
|
||||
:math:`u` and :math:`deg^{\leftrightarrow}(u)` is the reciprocal degree of
|
||||
:math:`u`.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
|
||||
nodes : container of nodes, optional (default=all nodes in G)
|
||||
Compute clustering for nodes in this container.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
The edge attribute that holds the numerical value used as a weight.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : float, or dictionary
|
||||
Clustering coefficient at specified nodes
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.complete_graph(5)
|
||||
>>> print(eg.clustering(G, 0))
|
||||
1.0
|
||||
>>> print(eg.clustering(G))
|
||||
{0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
|
||||
|
||||
Notes
|
||||
-----
|
||||
Self loops are ignored.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Generalizations of the clustering coefficient to weighted
|
||||
complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
|
||||
K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
|
||||
http://jponnela.com/web_documents/a9.pdf
|
||||
.. [2] Intensity and coherence of motifs in weighted complex
|
||||
networks by J. P. Onnela, J. Saramäki, J. Kertész, and K. Kaski,
|
||||
Physical Review E, 71(6), 065103 (2005).
|
||||
.. [3] Generalization of Clustering Coefficients to Signed Correlation Networks
|
||||
by G. Costantini and M. Perugini, PloS one, 9(2), e88669 (2014).
|
||||
.. [4] Clustering in complex directed networks by G. Fagiolo,
|
||||
Physical Review E, 76(2), 026107 (2007).
|
||||
"""
|
||||
|
||||
if G.is_directed():
|
||||
if weight is not None:
|
||||
td_iter = _directed_weighted_triangles_and_degree_iter(
|
||||
G, nodes, weight, n_workers=n_workers
|
||||
)
|
||||
clusterc = {
|
||||
v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
|
||||
for v, dt, db, t in td_iter
|
||||
}
|
||||
else:
|
||||
td_iter = _directed_triangles_and_degree_iter(G, nodes, n_workers=n_workers)
|
||||
clusterc = {
|
||||
v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
|
||||
for v, dt, db, t in td_iter
|
||||
}
|
||||
else:
|
||||
# The formula 2*T/(d*(d-1)) from docs is t/(d*(d-1)) here b/c t==2*T
|
||||
if weight is not None:
|
||||
td_iter = _weighted_triangles_and_degree_iter(
|
||||
G, nodes, weight, n_workers=n_workers
|
||||
)
|
||||
clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t in td_iter}
|
||||
else:
|
||||
td_iter = _triangles_and_degree_iter(G, nodes, n_workers=n_workers)
|
||||
clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t, _ in td_iter}
|
||||
if nodes in G:
|
||||
# Return the value of the sole entry in the dictionary.
|
||||
return clusterc[nodes]
|
||||
return clusterc
|
||||
@@ -0,0 +1,226 @@
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import scipy.sparse as sparse
|
||||
|
||||
|
||||
__all__ = [
|
||||
"localAssort",
|
||||
]
|
||||
|
||||
|
||||
def localAssort(
|
||||
edgelist, node_attr, pr=np.arange(0.0, 1.0, 0.1), undir=True, missingValue=-1
|
||||
):
|
||||
"""Calculate the multiscale assortativity.
|
||||
You must ensure that the node index and node attribute index start from 0
|
||||
Parameters
|
||||
----------
|
||||
edgelist : array_like
|
||||
the network represented as an edge list,
|
||||
i.e., a E x 2 array of node pairs
|
||||
node_attr : array_like
|
||||
n length array of node attribute values
|
||||
pr : array, optional
|
||||
array of one minus restart probabilities for the random walk in
|
||||
calculating the personalised pagerank. The largest of these values
|
||||
determines the accuracy of the TotalRank vector max(pr) -> 1 is more
|
||||
accurate (default: [0, .1, .2, .3, .4, .5, .6, .7, .8, .9])
|
||||
undir : bool, optional
|
||||
indicate if network is undirected (default: True)
|
||||
missingValue : int, optional
|
||||
token to indicate missing attribute values (default: -1)
|
||||
Returns
|
||||
-------
|
||||
assortM : array_like
|
||||
n x len(pr) array of local assortativities, each column corresponds to
|
||||
a value of the input restart probabilities, pr. Note if only number of
|
||||
restart probabilties is greater than one (i.e., len(pr) > 1).
|
||||
assortT : array_like
|
||||
n length array of multiscale assortativities
|
||||
Z : array_like
|
||||
N length array of per-node confidence scores
|
||||
References
|
||||
----------
|
||||
For full details see [1]_
|
||||
.. [1] Peel, L., Delvenne, J. C., & Lambiotte, R. (2018). "Multiscale
|
||||
mixing patterns in networks.' PNAS, 115(16), 4057-4062.
|
||||
"""
|
||||
# number of nodes
|
||||
n = len(node_attr)
|
||||
|
||||
# number of nodes with complete attribute
|
||||
ncomp = (node_attr != missingValue).sum()
|
||||
# number of edges
|
||||
m = len(edgelist)
|
||||
# construct adjacency matrix and calculate degree sequence
|
||||
A, degree = createA(edgelist, n, undir)
|
||||
|
||||
# construct diagonal inverse degree matrix
|
||||
D = sparse.diags(1.0 / degree, 0, format="csc")
|
||||
|
||||
# construct transition matrix (row normalised adjacency matrix)
|
||||
W = D @ A
|
||||
|
||||
# number of distinct node categories
|
||||
c = len(np.unique(node_attr))
|
||||
if ncomp < n:
|
||||
c -= 1
|
||||
|
||||
# calculate node weights for how "complete" the
|
||||
# metadata is around the node
|
||||
Z = np.zeros(n)
|
||||
|
||||
Z[node_attr == missingValue] = 1.0
|
||||
|
||||
Z = (W @ Z) / degree
|
||||
|
||||
# indicator array if node has attribute data (or missing)
|
||||
hasAttribute = node_attr != missingValue
|
||||
|
||||
# calculate global expected values
|
||||
values = np.ones(ncomp)
|
||||
|
||||
yi = (hasAttribute).nonzero()[0]
|
||||
|
||||
yj = node_attr[hasAttribute]
|
||||
Y = sparse.coo_matrix((values, (yi, yj)), shape=(n, c)).tocsc()
|
||||
eij_glob = np.array(Y.T @ (A @ Y).todense())
|
||||
|
||||
eij_glob /= np.sum(eij_glob)
|
||||
|
||||
ab_glob = np.sum(eij_glob.sum(1) * eij_glob.sum(0))
|
||||
# initialise outputs
|
||||
assortM = np.empty((n, len(pr)))
|
||||
assortT = np.empty(n)
|
||||
WY = (W @ Y).tocsc()
|
||||
|
||||
for i in range(n):
|
||||
pis, ti, it = calculateRWRrange(W, i, pr, n)
|
||||
if len(pr) > 1:
|
||||
for ii, pri in enumerate(pr):
|
||||
pi = pis[:, ii]
|
||||
|
||||
YPI = sparse.coo_matrix(
|
||||
(
|
||||
pi[hasAttribute],
|
||||
(node_attr[hasAttribute], np.arange(n)[hasAttribute]),
|
||||
),
|
||||
shape=(c, n),
|
||||
).tocsr()
|
||||
trace_e = (YPI.dot(WY).toarray()).trace()
|
||||
assortM[i, ii] = trace_e
|
||||
YPI = sparse.coo_matrix(
|
||||
(ti[hasAttribute], (node_attr[hasAttribute], np.arange(n)[hasAttribute])),
|
||||
shape=(c, n),
|
||||
).tocsr()
|
||||
e_gh = (YPI @ WY).toarray()
|
||||
e_gh_sum = e_gh.sum()
|
||||
Z[i] = e_gh_sum
|
||||
e_gh /= e_gh_sum
|
||||
trace_e = e_gh.trace()
|
||||
assortT[i] = trace_e
|
||||
|
||||
assortT -= ab_glob
|
||||
np.divide(assortT, 1.0 - ab_glob, out=assortT, where=ab_glob != 0)
|
||||
|
||||
if len(pr) > 1:
|
||||
assortM -= ab_glob
|
||||
np.divide(assortM, 1.0 - ab_glob, out=assortM, where=ab_glob != 0)
|
||||
return assortM, assortT, Z
|
||||
return None, assortT, Z
|
||||
|
||||
|
||||
def createA(E, n, undir=True):
|
||||
"""Create adjacency matrix and degree sequence."""
|
||||
if undir:
|
||||
G = eg.Graph()
|
||||
else:
|
||||
G = eg.DiGraph()
|
||||
G.add_nodes_from(range(n))
|
||||
|
||||
for e in E:
|
||||
G.add_edge(e[0], e[1])
|
||||
|
||||
A = eg.to_scipy_sparse_matrix(G)
|
||||
|
||||
degree = np.array(A.sum(1)).flatten()
|
||||
|
||||
return A, degree
|
||||
|
||||
|
||||
def calculateRWRrange(W, i, alphas, n, maxIter=1000):
|
||||
"""
|
||||
Calculate the personalised TotalRank and personalised PageRank vectors.
|
||||
Parameters
|
||||
----------
|
||||
W : array_like
|
||||
transition matrix (row normalised adjacency matrix)
|
||||
i : int
|
||||
index of the personalisation node
|
||||
alphas : array_like
|
||||
array of (1 - restart probabilties)
|
||||
n : int
|
||||
number of nodes in the network
|
||||
maxIter : int, optional
|
||||
maximum number of interations (default: 1000)
|
||||
Returns
|
||||
-------
|
||||
pPageRank_all : array_like
|
||||
personalised PageRank for all input alpha values (only calculated if
|
||||
more than one alpha given as input, i.e., len(alphas) > 1)
|
||||
pTotalRank : array_like
|
||||
personalised TotalRank (personalised PageRank with alpha integrated
|
||||
out)
|
||||
|
||||
it : int
|
||||
number of iterations
|
||||
References
|
||||
----------
|
||||
See [2]_ and [3]_ for further details.
|
||||
.. [2] Boldi, P. (2005). "TotalRank: Ranking without damping." In Special
|
||||
interest tracks and posters of the 14th international conference on
|
||||
World Wide Web (pp. 898-899).
|
||||
.. [3] Boldi, P., Santini, M., & Vigna, S. (2007). "A deeper investigation
|
||||
of PageRank as a function of the damping factor." In Dagstuhl Seminar
|
||||
Proceedings. Schloss Dagstuhl-Leibniz-Zentrum für Informatik.
|
||||
"""
|
||||
alpha0 = alphas.max()
|
||||
WT = alpha0 * W.T
|
||||
diff = 1
|
||||
it = 1
|
||||
|
||||
# initialise PageRank vectors
|
||||
pPageRank = np.zeros(n)
|
||||
|
||||
pPageRank_all = np.zeros((n, len(alphas)))
|
||||
pPageRank[i] = 1
|
||||
|
||||
pPageRank_all[i, :] = 1
|
||||
|
||||
pPageRank_old = pPageRank.copy()
|
||||
pTotalRank = pPageRank.copy()
|
||||
|
||||
oneminusalpha0 = 1 - alpha0
|
||||
|
||||
while diff > 1e-9:
|
||||
# calculate personalised PageRank via power iteration
|
||||
pPageRank = WT @ pPageRank
|
||||
pPageRank[i] += oneminusalpha0
|
||||
# calculate difference in pPageRank from previous iteration
|
||||
delta_pPageRank = pPageRank - pPageRank_old
|
||||
# Eq. [S23] Ref. [1]
|
||||
pTotalRank += (delta_pPageRank) / ((it + 1) * (alpha0**it))
|
||||
# only calculate personalised pageranks if more than one alpha
|
||||
if len(alphas) > 1:
|
||||
pPageRank_all += np.outer((delta_pPageRank), (alphas / alpha0) ** it)
|
||||
|
||||
# calculate convergence criteria
|
||||
diff = np.sum((delta_pPageRank) ** 2) / n
|
||||
it += 1
|
||||
|
||||
if it > maxIter:
|
||||
print(i, "max iterations exceeded")
|
||||
diff = 0
|
||||
pPageRank_old = pPageRank.copy()
|
||||
|
||||
return pPageRank_all, pTotalRank, it
|
||||
@@ -0,0 +1,101 @@
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = [
|
||||
"predecessor",
|
||||
]
|
||||
|
||||
|
||||
def predecessor(G, source, target=None, cutoff=None, return_seen=None):
|
||||
"""Returns dict of predecessors for the path from source to all nodes in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph graph
|
||||
|
||||
source : node label
|
||||
Starting node for path
|
||||
|
||||
target : node label, optional
|
||||
Ending node for path. If provided only predecessors between
|
||||
source and target are returned
|
||||
|
||||
cutoff : integer, optional
|
||||
Depth to stop the search. Only paths of length <= cutoff are returned.
|
||||
|
||||
return_seen : bool, optional (default=None)
|
||||
Whether to return a dictionary, keyed by node, of the level (number of
|
||||
hops) to reach the node (as seen during breadth-first-search).
|
||||
|
||||
Returns
|
||||
-------
|
||||
pred : dictionary
|
||||
Dictionary, keyed by node, of predecessors in the shortest path.
|
||||
|
||||
|
||||
(pred, seen): tuple of dictionaries
|
||||
If `return_seen` argument is set to `True`, then a tuple of dictionaries
|
||||
is returned. The first element is the dictionary, keyed by node, of
|
||||
predecessors in the shortest path. The second element is the dictionary,
|
||||
keyed by node, of the level (number of hops) to reach the node (as seen
|
||||
during breadth-first-search).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4)
|
||||
>>> list(G)
|
||||
[0, 1, 2, 3]
|
||||
>>> eg.predecessor(G, 0)
|
||||
{0: [], 1: [0], 2: [1], 3: [2]}
|
||||
>>> eg.predecessor(G, 0, return_seen=True)
|
||||
({0: [], 1: [0], 2: [1], 3: [2]}, {0: 0, 1: 1, 2: 2, 3: 3})
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if source not in G:
|
||||
raise eg.NodeNotFound(f"Source {source} not in G")
|
||||
level = 0 # the current level
|
||||
nextlevel = [source] # list of nodes to check at next level
|
||||
seen = {source: level} # level (number of hops) when seen in BFS
|
||||
pred = {source: []} # predecessor dictionary
|
||||
while nextlevel:
|
||||
level = level + 1
|
||||
thislevel = nextlevel
|
||||
nextlevel = []
|
||||
for v in thislevel:
|
||||
for w in list(G.neighbors(v)):
|
||||
if w not in seen:
|
||||
pred[w] = [v]
|
||||
seen[w] = level
|
||||
nextlevel.append(w)
|
||||
elif seen[w] == level: # add v to predecessor list if it
|
||||
pred[w].append(v) # is at the correct level
|
||||
if cutoff and cutoff <= level:
|
||||
break
|
||||
|
||||
if target is not None:
|
||||
if return_seen:
|
||||
if target not in pred:
|
||||
return ([], -1) # No predecessor
|
||||
return (pred[target], seen[target])
|
||||
else:
|
||||
if target not in pred:
|
||||
return [] # No predecessor
|
||||
return pred[target]
|
||||
else:
|
||||
if return_seen:
|
||||
return (pred, seen)
|
||||
else:
|
||||
return pred
|
||||
|
||||
|
||||
# def main():
|
||||
# G = eg.path_graph(4)
|
||||
# print(G.edges)
|
||||
|
||||
# print(predecessor(G, 0))
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# main()
|
||||
@@ -0,0 +1,41 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph.functions.basic import average_degree
|
||||
|
||||
|
||||
def test_average_degree_basic():
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
assert average_degree(G) == pytest.approx(4 / 3)
|
||||
|
||||
|
||||
def test_average_degree_empty_graph():
|
||||
G = eg.Graph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
average_degree(G)
|
||||
|
||||
|
||||
def test_average_degree_self_loop():
|
||||
G = eg.Graph()
|
||||
G.add_edge(1, 1) # self-loop
|
||||
# Self-loop counts as 2 towards degree of node 1
|
||||
assert average_degree(G) == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_average_degree_with_isolated_node():
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
G.add_node(4) # isolated node
|
||||
assert average_degree(G) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_average_degree_directed_graph():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (2, 3), (3, 1)])
|
||||
assert average_degree(G) == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_average_degree_invalid_input():
|
||||
with pytest.raises(AttributeError):
|
||||
average_degree(None)
|
||||
@@ -0,0 +1,418 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class TestClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edge("1", "2", weight=16)
|
||||
G.add_edge("2", "3", weight=16)
|
||||
G.add_edge("4", "3", weight=16)
|
||||
G.add_edge("3", "4", weight=23)
|
||||
G.add_edge("3", "5", weight=16)
|
||||
G.add_edge("4", "2", weight=20)
|
||||
print("clustering" in dir(eg))
|
||||
assert eg.clustering(G) == {
|
||||
"1": 0,
|
||||
"2": 0.3333333333333333,
|
||||
"3": 0.2,
|
||||
"4": 0.5,
|
||||
"5": 0,
|
||||
}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G) == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
|
||||
|
||||
def test_k5_signed(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
1 / 6,
|
||||
-1 / 3,
|
||||
1,
|
||||
3 / 6,
|
||||
3 / 6,
|
||||
]
|
||||
|
||||
|
||||
class TestDirectedClustering:
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
assert list(eg.clustering(G).values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G) == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
assert eg.clustering(G, 0) == 0
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
11 / 12,
|
||||
1,
|
||||
1,
|
||||
11 / 12,
|
||||
11 / 12,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 11 / 12}
|
||||
G.remove_edge(2, 1)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
|
||||
assert eg.clustering(G, 4) == 5 / 6
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4)
|
||||
assert eg.clustering(G)[0] == 1 / 6
|
||||
|
||||
|
||||
class TestDirectedAverageClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_empty(self):
|
||||
G = eg.DiGraph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
eg.average_clustering(G)
|
||||
|
||||
def test_average_clustering(self):
|
||||
G = eg.empty_graph(range(3), eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(2, 3)
|
||||
assert eg.average_clustering(G) == (1 + 1 + 1 / 3) / 8
|
||||
assert eg.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 8
|
||||
assert eg.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 4
|
||||
|
||||
|
||||
class TestAverageClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_empty(self):
|
||||
G = eg.Graph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
eg.average_clustering(G)
|
||||
|
||||
def test_average_clustering(self):
|
||||
G = eg.complete_graph(3)
|
||||
G.add_edge(2, 3)
|
||||
|
||||
assert eg.average_clustering(G) == (1 + 1 + 1 / 3) / 4
|
||||
assert eg.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 4
|
||||
assert eg.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 2
|
||||
|
||||
def test_average_clustering_signed(self):
|
||||
G = eg.complete_graph(3)
|
||||
G.add_edge(2, 3)
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
assert eg.average_clustering(G, weight="weight") == (-1 - 1 - 1 / 3) / 4
|
||||
assert (
|
||||
eg.average_clustering(G, weight="weight", count_zeros=True)
|
||||
== (-1 - 1 - 1 / 3) / 4
|
||||
)
|
||||
assert (
|
||||
eg.average_clustering(G, weight="weight", count_zeros=False)
|
||||
== (-1 - 1 - 1 / 3) / 3
|
||||
)
|
||||
|
||||
|
||||
class TestDirectedWeightedClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
assert list(eg.clustering(G, weight="weight").values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10, create_using=eg.DiGraph())
|
||||
print("type:", eg.clustering(G, weight="weight"))
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, weight="weight") == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G, weight="weight") == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
11 / 12,
|
||||
1,
|
||||
1,
|
||||
11 / 12,
|
||||
11 / 12,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {1: 1, 4: 11 / 12}
|
||||
G.remove_edge(2, 1)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {
|
||||
1: 1,
|
||||
4: 0.83333333333333337,
|
||||
}
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), create_using=eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4, weight=2)
|
||||
assert eg.clustering(G)[0] == 1 / 6
|
||||
# Relaxed comparisons to allow graphblas-algorithms to pass tests
|
||||
np.testing.assert_allclose(eg.clustering(G, weight="weight")[0], 1 / 12)
|
||||
np.testing.assert_allclose(eg.clustering(G, 0, weight="weight"), 1 / 12)
|
||||
|
||||
|
||||
class TestWeightedClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.Graph()
|
||||
assert list(eg.clustering(G, weight="weight").values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, weight="weight") == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_cubical(self):
|
||||
G = eg.from_dict_of_lists(
|
||||
{
|
||||
0: [1, 3, 4],
|
||||
1: [0, 2, 7],
|
||||
2: [1, 3, 6],
|
||||
3: [0, 2, 5],
|
||||
4: [0, 5, 7],
|
||||
5: [3, 4, 6],
|
||||
6: [2, 5, 7],
|
||||
7: [1, 4, 6],
|
||||
},
|
||||
create_using=None,
|
||||
)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, 1) == 0
|
||||
assert list(eg.clustering(G, [1, 2], weight="weight").values()) == [0, 0]
|
||||
assert eg.clustering(G, 1, weight="weight") == 0
|
||||
assert eg.clustering(G, [1, 2], weight="weight") == {1: 0, 2: 0}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G, weight="weight") == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {
|
||||
1: 1,
|
||||
4: 0.83333333333333337,
|
||||
}
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), None)
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4, weight=2)
|
||||
assert eg.clustering(G)[0] == 1 / 3
|
||||
np.testing.assert_allclose(eg.clustering(G, weight="weight")[0], 1 / 6)
|
||||
np.testing.assert_allclose(eg.clustering(G, 0, weight="weight"), 1 / 6)
|
||||
|
||||
def test_triangle_and_signed_edge(self):
|
||||
G = eg.empty_graph(range(3), None)
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
G.add_edge(3, 0, weight=0)
|
||||
assert eg.clustering(G)[0] == 1 / 3
|
||||
assert eg.clustering(G, weight="weight")[0] == -1 / 3
|
||||
|
||||
|
||||
class TestAdditionalClusteringCases:
|
||||
def test_self_loops_ignored(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
G.add_edge(0, 0) # self-loop
|
||||
assert eg.clustering(G, 0) == 1.0
|
||||
|
||||
def test_isolated_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1)
|
||||
assert eg.clustering(G) == {1: 0}
|
||||
|
||||
def test_degree_one_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_edge(1, 2)
|
||||
assert eg.clustering(G) == {1: 0, 2: 0}
|
||||
|
||||
def test_custom_weight_name(self):
|
||||
G = eg.Graph()
|
||||
G.add_edge(0, 1, strength=2)
|
||||
G.add_edge(1, 2, strength=2)
|
||||
G.add_edge(2, 0, strength=2)
|
||||
result = eg.clustering(G, weight="strength")
|
||||
assert result[0] > 0
|
||||
|
||||
def test_negative_weights_mixed(self):
|
||||
G = eg.complete_graph(3)
|
||||
G[0][1]["weight"] = -1
|
||||
G[1][2]["weight"] = 1
|
||||
G[2][0]["weight"] = 1
|
||||
assert eg.clustering(G, 0, weight="weight") < 0
|
||||
|
||||
def test_directed_reciprocal_edges(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)])
|
||||
result = eg.clustering(G)
|
||||
assert all(0 <= v <= 1 for v in result.values())
|
||||
@@ -0,0 +1,104 @@
|
||||
import sys
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from easygraph.functions.basic.localassort import localAssort
|
||||
|
||||
|
||||
class TestLocalAssort:
|
||||
@classmethod
|
||||
def setup_class(self):
|
||||
self.G = eg.get_graph_karateclub()
|
||||
edgelist = []
|
||||
node_num = len(self.G.nodes)
|
||||
for e in self.G.edges:
|
||||
edgelist.append([e[0] - 1, e[1] - 1])
|
||||
self.edgelist = np.int32(edgelist)
|
||||
self.valuelist = np.arange(node_num, dtype=np.int32) % 6
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info.major <= 3 and sys.version_info.minor <= 7,
|
||||
reason="python version should higher than 3.7",
|
||||
)
|
||||
def test_karateclub(self):
|
||||
assortM, assortT, Z = eg.localAssort(
|
||||
self.edgelist, self.valuelist, pr=np.arange(0, 1, 0.1)
|
||||
)
|
||||
|
||||
_, assortT, Z = eg.functions.basic.localassort.localAssort(
|
||||
self.edgelist, self.valuelist, pr=np.array([0.9])
|
||||
)
|
||||
|
||||
|
||||
def test_localassort_small_complete_graph():
|
||||
G = eg.complete_graph(4)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, 0, 1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (4, 10)
|
||||
assert assortT.shape == (4,)
|
||||
assert Z.shape == (4,)
|
||||
assert np.all(Z >= 0) and np.all(Z <= 1)
|
||||
|
||||
|
||||
def test_localassort_with_missing_attributes():
|
||||
G = eg.path_graph(5)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, -1, 1, -1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, pr=np.array([0.5]))
|
||||
assert assortT.shape == (5,)
|
||||
assert Z.shape == (5,)
|
||||
assert np.any(np.isnan(assortT))
|
||||
|
||||
|
||||
def test_localassort_directed_graph():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, 1, 0, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, undir=False)
|
||||
assert assortM.shape == (4, 10)
|
||||
assert assortT.shape == (4,)
|
||||
assert Z.shape == (4,)
|
||||
|
||||
|
||||
def test_localassort_single_node_graph():
|
||||
edgelist = np.empty((0, 2), dtype=int)
|
||||
node_attr = np.array([0])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (1, 10)
|
||||
assert np.all(np.isnan(assortM)) or np.allclose(assortM, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(assortT)) or np.allclose(assortT, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(Z)) or np.allclose(Z, 0, atol=1e-5)
|
||||
|
||||
|
||||
def test_localassort_disconnected_graph():
|
||||
G = eg.Graph()
|
||||
G.add_nodes_from(range(5))
|
||||
edgelist = np.empty((0, 2), dtype=int)
|
||||
node_attr = np.array([0, 1, 0, 1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (5, 10)
|
||||
assert np.all(np.isnan(assortM)) or np.allclose(assortM, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(assortT)) or np.allclose(assortT, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(Z)) or np.allclose(Z, 0, atol=1e-5)
|
||||
|
||||
|
||||
def test_localassort_high_restart_probabilities():
|
||||
G = eg.path_graph(5)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([1, 0, 1, 0, 1])
|
||||
pr = np.array([0.95, 0.99])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, pr=pr)
|
||||
assert assortM.shape == (5, 2)
|
||||
assert assortT.shape == (5,)
|
||||
assert Z.shape == (5,)
|
||||
|
||||
|
||||
def test_localassort_invalid_attribute_length():
|
||||
edgelist = np.array([[0, 1], [1, 2]])
|
||||
node_attr = np.array([0, 1]) # too short
|
||||
with pytest.raises(ValueError):
|
||||
localAssort(edgelist, node_attr)
|
||||
@@ -0,0 +1,79 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class TestPredecessor:
|
||||
# @classmethod
|
||||
# def setup_class(self):
|
||||
# pytest.importskip("numpy")
|
||||
|
||||
def test_predecessor(self):
|
||||
G = eg.path_graph(4)
|
||||
for source in G:
|
||||
assert eg.predecessor(G, source) in [
|
||||
{0: [], 1: [0], 2: [1], 3: [2]},
|
||||
{1: [], 0: [1], 2: [1], 3: [2]},
|
||||
{2: [], 1: [2], 3: [2], 0: [1]},
|
||||
{3: [], 2: [3], 1: [2], 0: [1]},
|
||||
]
|
||||
|
||||
def test_basic_predecessor(self):
|
||||
G = eg.path_graph(4)
|
||||
result = eg.predecessor(G, 0)
|
||||
assert result == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
|
||||
def test_with_return_seen(self):
|
||||
G = eg.path_graph(4)
|
||||
pred, seen = eg.predecessor(G, 0, return_seen=True)
|
||||
assert pred == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
assert seen == {0: 0, 1: 1, 2: 2, 3: 3}
|
||||
|
||||
def test_with_target(self):
|
||||
G = eg.path_graph(4)
|
||||
assert eg.predecessor(G, 0, target=2) == [1]
|
||||
|
||||
def test_with_target_and_return_seen(self):
|
||||
G = eg.path_graph(4)
|
||||
pred, seen = eg.predecessor(G, 0, target=2, return_seen=True)
|
||||
assert pred == [1]
|
||||
assert seen == 2
|
||||
|
||||
def test_with_cutoff(self):
|
||||
G = eg.path_graph(4)
|
||||
pred = eg.predecessor(G, 0, cutoff=1)
|
||||
assert pred == {0: [], 1: [0]}
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert 2 not in pred and 3 not in pred
|
||||
|
||||
def test_invalid_source(self):
|
||||
G = eg.path_graph(4)
|
||||
with pytest.raises(eg.NodeNotFound):
|
||||
eg.predecessor(G, 99)
|
||||
|
||||
def test_no_path_to_target(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
assert eg.predecessor(G, 0, target=3) == []
|
||||
|
||||
def test_no_path_to_target_with_return_seen(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
pred, seen = eg.predecessor(G, 0, target=3, return_seen=True)
|
||||
assert pred == []
|
||||
assert seen == -1
|
||||
|
||||
def test_cycle_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) # cycled graph
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert set(pred.keys()) == set(G.nodes)
|
||||
|
||||
def test_directed_graph(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert pred == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
@@ -0,0 +1,9 @@
|
||||
from .betweenness import *
|
||||
from .closeness import *
|
||||
from .degree import *
|
||||
from .ego_betweenness import *
|
||||
from .flowbetweenness import *
|
||||
from .laplacian import *
|
||||
from .pagerank import *
|
||||
from .katz_centrality import *
|
||||
from .eigenvector import *
|
||||
@@ -0,0 +1,245 @@
|
||||
from easygraph.utils import *
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"betweenness_centrality",
|
||||
]
|
||||
|
||||
|
||||
def betweenness_centrality_parallel(nodes, G, path_length, accumulate):
|
||||
betweenness = {node: 0.0 for node in G}
|
||||
for node in nodes:
|
||||
S, P, sigma = path_length(G, source=node)
|
||||
betweenness = accumulate(betweenness, S, P, sigma, node)
|
||||
return betweenness
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_betweenness_centrality")
|
||||
def betweenness_centrality(
|
||||
G, weight=None, sources=None, normalized=True, endpoints=False, n_workers=None
|
||||
):
|
||||
r"""Compute the shortest-basic betweenness centrality for nodes.
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(v) = \sum_{s,t \in V} \frac{\sigma(s, t|v)}{\sigma(s, t)}
|
||||
|
||||
where V is the set of nodes,
|
||||
|
||||
.. math::
|
||||
\sigma(s, t)
|
||||
|
||||
is the number of shortest (s, t)-paths, and
|
||||
|
||||
.. math::
|
||||
|
||||
\sigma(s, t|v)
|
||||
|
||||
is the number of those paths passing through some node v other than s, t.
|
||||
|
||||
.. math::
|
||||
|
||||
If\ s\ =\ t,\ \sigma(s, t) = 1, and\ if\ v \in {s, t}, \sigma(s, t|v) = 0 [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
|
||||
sources : None or nodes list, optional (default=None)
|
||||
If None, all nodes are considered.
|
||||
Otherwise,the set of source vertices to consider when calculating shortest paths.
|
||||
|
||||
normalized : bool, optional
|
||||
If True the betweenness values are normalized by `2/((n-1)(n-2))`
|
||||
for graphs, and `1/((n-1)(n-2))` for directed graphs where `n`
|
||||
is the number of nodes in G.
|
||||
|
||||
endpoints : bool, optional
|
||||
If True include the endpoints in the shortest basic counts.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with betweenness centrality as the value.
|
||||
|
||||
>>> betweenness_centrality(G,weight="weight")
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
if weight is not None:
|
||||
path_length = functools.partial(_single_source_dijkstra_path, weight=weight)
|
||||
else:
|
||||
path_length = functools.partial(_single_source_bfs_path)
|
||||
|
||||
if endpoints:
|
||||
accumulate = functools.partial(_accumulate_endpoints)
|
||||
else:
|
||||
accumulate = functools.partial(_accumulate_basic)
|
||||
|
||||
if sources is not None:
|
||||
nodes = sources
|
||||
else:
|
||||
nodes = G.nodes
|
||||
betweenness = dict.fromkeys(G, 0.0)
|
||||
|
||||
if n_workers is not None:
|
||||
# use the parallel version for large graph
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
nodes = list(nodes)
|
||||
random.shuffle(nodes)
|
||||
|
||||
if len(nodes) > n_workers * 30000:
|
||||
nodes = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes = split(nodes, n_workers)
|
||||
local_function = partial(
|
||||
betweenness_centrality_parallel,
|
||||
G=G,
|
||||
path_length=path_length,
|
||||
accumulate=accumulate,
|
||||
)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(local_function, nodes)
|
||||
for res in ret:
|
||||
for key in res:
|
||||
betweenness[key] += res[key]
|
||||
else:
|
||||
# use np-parallel version for small graph
|
||||
for node in nodes:
|
||||
S, P, sigma = path_length(G, source=node)
|
||||
betweenness = accumulate(betweenness, S, P, sigma, node)
|
||||
|
||||
betweenness = _rescale(
|
||||
betweenness,
|
||||
len(G),
|
||||
normalized=normalized,
|
||||
directed=G.is_directed(),
|
||||
endpoints=endpoints,
|
||||
)
|
||||
ret = [0.0 for i in range(len(G))]
|
||||
for i in range(len(ret)):
|
||||
ret[i] = betweenness[G.index2node[i]]
|
||||
return ret
|
||||
|
||||
|
||||
def _rescale(betweenness, n, normalized, directed=False, endpoints=False):
|
||||
if normalized:
|
||||
if endpoints:
|
||||
if n < 2:
|
||||
scale = None # no normalization
|
||||
else:
|
||||
# Scale factor should include endpoint nodes
|
||||
scale = 1 / (n * (n - 1))
|
||||
elif n <= 2:
|
||||
scale = None # no normalization b=0 for all nodes
|
||||
else:
|
||||
scale = 1 / ((n - 1) * (n - 2))
|
||||
else: # rescale by 2 for undirected graphs
|
||||
if not directed:
|
||||
scale = 0.5
|
||||
else:
|
||||
scale = None
|
||||
if scale is not None:
|
||||
for v in betweenness:
|
||||
betweenness[v] *= scale
|
||||
return betweenness
|
||||
|
||||
|
||||
def _single_source_bfs_path(G, source):
|
||||
S = []
|
||||
P = {v: [] for v in G}
|
||||
sigma = dict.fromkeys(G, 0.0)
|
||||
D = {}
|
||||
sigma[source] = 1.0
|
||||
D[source] = 0
|
||||
Q = [source]
|
||||
adj = G.adj
|
||||
while Q:
|
||||
v = Q.pop(0)
|
||||
S.append(v)
|
||||
Dv = D[v]
|
||||
sigmav = sigma[v]
|
||||
for w in adj[v]:
|
||||
if w not in D:
|
||||
Q.append(w)
|
||||
D[w] = Dv + 1
|
||||
if D[w] == Dv + 1:
|
||||
sigma[w] += sigmav
|
||||
P[w].append(v)
|
||||
return S, P, sigma
|
||||
|
||||
|
||||
def _single_source_dijkstra_path(G, source, weight="weight"):
|
||||
from heapq import heappop
|
||||
from heapq import heappush
|
||||
|
||||
push = heappush
|
||||
pop = heappop
|
||||
S = []
|
||||
P = {v: [] for v in G}
|
||||
sigma = dict.fromkeys(G, 0.0)
|
||||
D = {}
|
||||
sigma[source] = 1.0
|
||||
seen = {source: 0}
|
||||
Q = []
|
||||
from itertools import count
|
||||
|
||||
c = count()
|
||||
adj = G.adj
|
||||
push(Q, (0, next(c), source, source))
|
||||
while Q:
|
||||
(dist, _, pred, v) = pop(Q)
|
||||
if v in D:
|
||||
continue
|
||||
sigma[v] += sigma[pred]
|
||||
S.append(v)
|
||||
D[v] = dist
|
||||
for w in adj[v]:
|
||||
vw_dist = dist + adj[v][w].get(weight, 1)
|
||||
if w not in D and (w not in seen or vw_dist < seen[w]):
|
||||
seen[w] = vw_dist
|
||||
push(Q, (vw_dist, next(c), v, w))
|
||||
sigma[w] = 0.0
|
||||
P[w] = [v]
|
||||
elif vw_dist == seen[w]: # handle equal paths
|
||||
sigma[w] += sigma[v]
|
||||
P[w].append(v)
|
||||
return S, P, sigma
|
||||
|
||||
|
||||
def _accumulate_endpoints(betweenness, S, P, sigma, s):
|
||||
betweenness[s] += len(S) - 1
|
||||
delta = dict.fromkeys(S, 0)
|
||||
while S:
|
||||
w = S.pop()
|
||||
coeff = (1 + delta[w]) / sigma[w]
|
||||
for v in P[w]:
|
||||
delta[v] += sigma[v] * coeff
|
||||
if w != s:
|
||||
betweenness[w] += delta[w] + 1
|
||||
return betweenness
|
||||
|
||||
|
||||
def _accumulate_basic(betweenness, S, P, sigma, s):
|
||||
delta = dict.fromkeys(S, 0)
|
||||
while S:
|
||||
w = S.pop()
|
||||
coeff = (1 + delta[w]) / sigma[w]
|
||||
for v in P[w]:
|
||||
delta[v] += sigma[v] * coeff
|
||||
if w != s:
|
||||
betweenness[w] += delta[w]
|
||||
return betweenness
|
||||
@@ -0,0 +1,105 @@
|
||||
from easygraph.functions.basic import *
|
||||
from easygraph.functions.path import single_source_bfs
|
||||
from easygraph.functions.path import single_source_dijkstra
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"closeness_centrality",
|
||||
]
|
||||
|
||||
|
||||
def closeness_centrality_parallel(nodes, G, path_length):
|
||||
ret = []
|
||||
length = len(G)
|
||||
for node in nodes:
|
||||
x = path_length(G, node)
|
||||
dist = sum(x.values())
|
||||
cnt = len(x)
|
||||
if dist == 0:
|
||||
ret.append([node, 0])
|
||||
else:
|
||||
ret.append([node, (cnt - 1) * (cnt - 1) / (dist * (length - 1))])
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_closeness_centrality")
|
||||
def closeness_centrality(G, weight=None, sources=None, n_workers=None):
|
||||
r"""
|
||||
Compute closeness centrality for nodes.
|
||||
|
||||
.. math::
|
||||
|
||||
C_{WF}(u) = \frac{n-1}{N-1} \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
|
||||
|
||||
Notice that the closeness distance function computes the
|
||||
outcoming distance to `u` for directed graphs. To use
|
||||
incoming distance, act on `G.reverse()`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
|
||||
sources : None or nodes list, optional (default=None)
|
||||
If None, all nodes are returned
|
||||
Otherwise,the set of source vertices to creturn.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with closeness centrality as the value.
|
||||
"""
|
||||
closeness = dict()
|
||||
if sources is not None:
|
||||
nodes = sources
|
||||
else:
|
||||
nodes = G.nodes
|
||||
length = len(G)
|
||||
import functools
|
||||
|
||||
if weight is not None:
|
||||
path_length = functools.partial(single_source_dijkstra, weight=weight)
|
||||
else:
|
||||
path_length = functools.partial(single_source_bfs)
|
||||
|
||||
if n_workers is not None:
|
||||
# use parallel version for large graph
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
nodes = list(nodes)
|
||||
random.shuffle(nodes)
|
||||
|
||||
if len(nodes) > n_workers * 30000:
|
||||
nodes = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes = split(nodes, n_workers)
|
||||
local_function = partial(
|
||||
closeness_centrality_parallel, G=G, path_length=path_length
|
||||
)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(local_function, nodes)
|
||||
res = [x for i in ret for x in i]
|
||||
closeness = dict(res)
|
||||
else:
|
||||
# use np-parallel version for small graph
|
||||
for node in nodes:
|
||||
x = path_length(G, node)
|
||||
dist = sum(x.values())
|
||||
cnt = len(x)
|
||||
if dist == 0:
|
||||
closeness[node] = 0
|
||||
else:
|
||||
closeness[node] = (cnt - 1) * (cnt - 1) / (dist * (length - 1))
|
||||
ret = [0.0 for i in range(len(G))]
|
||||
for i in range(len(ret)):
|
||||
ret[i] = closeness[G.index2node[i]]
|
||||
return ret
|
||||
@@ -0,0 +1,125 @@
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = ["degree_centrality", "in_degree_centrality", "out_degree_centrality"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_degree_centrality")
|
||||
def degree_centrality(G):
|
||||
"""Compute the degree centrality for nodes in a bipartite network.
|
||||
|
||||
The degree centrality for a node v is the fraction of nodes it
|
||||
is connected to.
|
||||
|
||||
parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with degree centrality as the value.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The degree centrality are normalized by dividing by n-1 where
|
||||
n is number of nodes in G.
|
||||
"""
|
||||
if len(G) <= 1:
|
||||
return {n: 1 for n in G}
|
||||
|
||||
s = 1.0 / (len(G) - 1.0)
|
||||
centrality = {n: d * s for n, d in (G.degree()).items()}
|
||||
return centrality
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@only_implemented_for_Directed_graph
|
||||
@hybrid("cpp_in_degree_centrality")
|
||||
def in_degree_centrality(G):
|
||||
"""Compute the in-degree centrality for nodes.
|
||||
|
||||
The in-degree centrality for a node v is the fraction of nodes its
|
||||
incoming edges are connected to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with in-degree centrality as values.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented:
|
||||
If G is undirected.
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_centrality, out_degree_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The degree centrality values are normalized by dividing by the maximum
|
||||
possible degree in a simple graph n-1 where n is the number of nodes in G.
|
||||
|
||||
For multigraphs or graphs with self loops the maximum degree might
|
||||
be higher than n-1 and values of degree centrality greater than 1
|
||||
are possible.
|
||||
"""
|
||||
if len(G) <= 1:
|
||||
return {n: 1 for n in G}
|
||||
|
||||
s = 1.0 / (len(G) - 1.0)
|
||||
centrality = {n: d * s for n, d in G.in_degree().items()}
|
||||
return centrality
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@only_implemented_for_Directed_graph
|
||||
@hybrid("cpp_out_degree_centrality")
|
||||
def out_degree_centrality(G):
|
||||
"""Compute the out-degree centrality for nodes.
|
||||
|
||||
The out-degree centrality for a node v is the fraction of nodes its
|
||||
outgoing edges are connected to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with out-degree centrality as values.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented:
|
||||
If G is undirected.
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_centrality, in_degree_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The degree centrality values are normalized by dividing by the maximum
|
||||
possible degree in a simple graph n-1 where n is the number of nodes in G.
|
||||
|
||||
For multigraphs or graphs with self loops the maximum degree might
|
||||
be higher than n-1 and values of degree centrality greater than 1
|
||||
are possible.
|
||||
"""
|
||||
if len(G) <= 1:
|
||||
return {n: 1 for n in G}
|
||||
|
||||
s = 1.0 / (len(G) - 1.0)
|
||||
centrality = {n: d * s for n, d in G.out_degree().items()}
|
||||
return centrality
|
||||
@@ -0,0 +1,57 @@
|
||||
__all__ = ["ego_betweenness"]
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def ego_betweenness(G, node):
|
||||
"""
|
||||
ego networks are networks consisting of a single actor (ego) together with the actors they are connected to (alters) and all the links among those alters.[1]
|
||||
Burt (1992), in his book Structural Holes, provides ample evidence that having high betweenness centrality, which is highly correlated with having many structural holes, can bring benefits to ego.[1]
|
||||
Returns the betweenness centrality of a ego network whose ego is set
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
node : int
|
||||
|
||||
Returns
|
||||
-------
|
||||
sum : float
|
||||
the betweenness centrality of a ego network whose ego is set
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns the betwenness centrality of node 1.
|
||||
|
||||
>>> ego_betweenness(G,node=1)
|
||||
|
||||
Reference
|
||||
---------
|
||||
.. [1] Martin Everett, Stephen P. Borgatti. "Ego network betweenness." Social Networks, Volume 27, Issue 1, Pages 31-38, 2005.
|
||||
|
||||
"""
|
||||
g = G.ego_subgraph(node)
|
||||
print(g.edges)
|
||||
print(g.nodes)
|
||||
n = len(g)
|
||||
|
||||
A = np.zeros((n, n))
|
||||
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
if g.has_edge(g.index2node[i], g.index2node[j]):
|
||||
A[i, j] = 1
|
||||
|
||||
B = A * A
|
||||
C = np.identity(n) - A
|
||||
sum = 0
|
||||
flag = G.is_directed()
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
if i != j and C[i, j] == 1 and B[i, j] != 0:
|
||||
sum += 1.0 / B[i, j]
|
||||
if flag == False:
|
||||
sum /= 2
|
||||
return sum
|
||||
@@ -0,0 +1,154 @@
|
||||
import math
|
||||
import easygraph as eg
|
||||
from easygraph.utils import *
|
||||
from easygraph.utils.decorators import *
|
||||
from scipy import sparse
|
||||
from scipy.sparse import linalg
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
|
||||
__all__ = ["eigenvector_centrality"]
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_eigenvector_centrality")
|
||||
def eigenvector_centrality(G, max_iter=100, tol=1.0e-6, nstart=None, weight=None):
|
||||
"""Calculate eigenvector centrality for nodes in the graph
|
||||
|
||||
Eigenvector centrality is based on the idea that a node's importance
|
||||
depends on the importance of its neighboring nodes.
|
||||
Specifically, a node's centrality is proportional to the sum of
|
||||
centrality values of its neighbors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph object
|
||||
An undirected or directed graph
|
||||
|
||||
max_iter : int, optional (default=100)
|
||||
Maximum number of iterations for the power method
|
||||
|
||||
tol : float, optional (default=1.0e-6)
|
||||
Convergence threshold; algorithm terminates when the difference
|
||||
between centrality values in consecutive iterations is less than this value
|
||||
|
||||
nstart : dictionary, optional (default=None)
|
||||
Dictionary mapping nodes to initial centrality values
|
||||
If None, the ARPACK solver is used to directly compute the eigenvector
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
Name of the edge attribute to be used as edge weight
|
||||
If None, all edges are considered to have weight 1
|
||||
|
||||
Returns
|
||||
-------
|
||||
centrality : dictionary
|
||||
Dictionary mapping nodes to their eigenvector centrality values
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphPointlessConcept
|
||||
When input is an empty graph
|
||||
|
||||
EasyGraphError
|
||||
When the algorithm fails to converge within the specified maximum iterations
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm uses the power iteration method to find the principal eigenvector.
|
||||
When nstart is not provided, the ARPACK solver is used for efficiency.
|
||||
The returned centrality values are normalized.
|
||||
"""
|
||||
|
||||
if len(G) == 0:
|
||||
raise eg.EasyGraphPointlessConcept(
|
||||
"cannot compute centrality for the null graph"
|
||||
)
|
||||
|
||||
if len(G) == 1:
|
||||
raise eg.EasyGraphPointlessConcept(
|
||||
"cannot compute eigenvector centrality for a single node graph"
|
||||
)
|
||||
|
||||
|
||||
# Build node list and mapping
|
||||
nodelist = list(G.nodes)
|
||||
n = len(nodelist)
|
||||
node_map = {node: i for i, node in enumerate(nodelist)}
|
||||
|
||||
# Build weighted adjacency matrix
|
||||
row, col, data = [], [], []
|
||||
for u in nodelist:
|
||||
u_idx = node_map[u]
|
||||
for v, attrs in G[u].items():
|
||||
if v in node_map:
|
||||
v_idx = node_map[v]
|
||||
w = attrs.get(weight, 1.0) if weight else 1.0
|
||||
# Build transpose matrix for centrality calculation
|
||||
row.append(v_idx)
|
||||
col.append(u_idx)
|
||||
data.append(float(w))
|
||||
|
||||
# Create CSR format sparse matrix
|
||||
A = sparse.csr_matrix((data, (row, col)), shape=(n, n))
|
||||
|
||||
# Detect and handle isolated nodes
|
||||
row_sums = np.array(A.sum(axis=1)).flatten()
|
||||
col_sums = np.array(A.sum(axis=0)).flatten()
|
||||
isolated_nodes = np.where((row_sums == 0) & (col_sums == 0))[0]
|
||||
|
||||
has_isolated = len(isolated_nodes) > 0
|
||||
isolated_indices = []
|
||||
|
||||
# Add small self-loops to isolated nodes for stability
|
||||
if has_isolated:
|
||||
# Store isolated node indices
|
||||
isolated_indices = isolated_nodes.tolist()
|
||||
|
||||
# Add small self-loop weights to isolated nodes
|
||||
for idx in isolated_indices:
|
||||
A[idx, idx] = 1.0e-4 # Small enough to not affect results, but maintains numerical stability
|
||||
if nstart is not None:
|
||||
# Use custom initial vector for power iteration
|
||||
v = np.array([nstart.get(n, 1.0) for n in nodelist], dtype=float)
|
||||
v = v / np.sum(np.abs(v))
|
||||
|
||||
# Power iteration method to compute principal eigenvector
|
||||
v_last = np.zeros_like(v)
|
||||
for _ in range(max_iter):
|
||||
np.copyto(v_last, v)
|
||||
v = A @ v_last # Sparse matrix multiplication
|
||||
|
||||
norm = np.linalg.norm(v)
|
||||
if norm < 1e-10:
|
||||
v = v_last.copy()
|
||||
break
|
||||
v = v / norm # Normalization
|
||||
|
||||
# Check convergence
|
||||
if np.linalg.norm(v - v_last) < tol:
|
||||
break
|
||||
else:
|
||||
raise eg.EasyGraphError(f"Eigenvector calculation did not converge in {max_iter} iterations")
|
||||
|
||||
centrality = v
|
||||
else:
|
||||
# Use ARPACK solver to directly compute the principal eigenvector
|
||||
eigenvalues, eigenvectors = linalg.eigs(A, k=1, which='LR',
|
||||
maxiter=max_iter, tol=tol)
|
||||
centrality = np.real(eigenvectors[:,0])
|
||||
|
||||
# Ensure positive results and normalize
|
||||
if centrality.sum() < 0:
|
||||
centrality = -centrality
|
||||
|
||||
centrality = centrality / np.linalg.norm(centrality)
|
||||
# Set centrality of isolated nodes to zero
|
||||
if has_isolated:
|
||||
for idx in isolated_indices:
|
||||
centrality[idx] = 0.0
|
||||
# Renormalize if needed
|
||||
if np.sum(centrality) > 0:
|
||||
centrality = centrality / np.linalg.norm(centrality)
|
||||
|
||||
# Return dictionary of node centrality values
|
||||
return {nodelist[i]: float(centrality[i]) for i in range(n)}
|
||||
@@ -0,0 +1,146 @@
|
||||
import collections
|
||||
import copy
|
||||
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"flowbetweenness_centrality",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def flowbetweenness_centrality(G):
|
||||
"""Compute the independent-basic betweenness centrality for nodes in a flow network.
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(v) =\\sum_{s,t \\in V} \frac{\\sigma(s, t|v)}{\\sigma(s, t)}
|
||||
|
||||
where V is the set of nodes,
|
||||
|
||||
.. math::
|
||||
|
||||
\\sigma(s, t)\\ is\\ the\\ number\\ of\\ independent\\ (s, t)-paths,
|
||||
|
||||
.. math::
|
||||
|
||||
\\sigma(s, t|v)\\ is\\ the\\ maximum\\ number\\ possible\\ of\\ those\\ paths\\ passing\\ through\\ some\\ node\\ v\\ other\\ than\\ s, t.\
|
||||
|
||||
.. math::
|
||||
|
||||
If\\ s\\ =\\ t,\\ \\sigma(s, t)\\ =\\ 1,\\ and\\ if\\ v \\in \\{s, t\\},\\ \\sigma(s, t|v)\\ =\\ 0\\ [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with independent-basic betweenness centrality as the value.
|
||||
|
||||
Notes
|
||||
-----
|
||||
A flow network is a directed graph where each edge has a capacity and each edge receives a flow.
|
||||
"""
|
||||
if G.is_directed() == False:
|
||||
print("Please input a directed graph")
|
||||
return
|
||||
flow_dict = NumberOfFlow(G)
|
||||
nodes = G.nodes
|
||||
result_dict = dict()
|
||||
for node, _ in nodes.items():
|
||||
result_dict[node] = 0
|
||||
for node_v, _ in nodes.items():
|
||||
for node_s, _ in nodes.items():
|
||||
for node_t, _ in nodes.items():
|
||||
num = 1
|
||||
num_v = 0
|
||||
if node_s == node_t:
|
||||
num_v = 0
|
||||
num = 1
|
||||
if node_v in [node_s, node_t]:
|
||||
num_v = 0
|
||||
num = 1
|
||||
if node_v != node_s and node_v != node_t and node_s != node_t:
|
||||
num = flow_dict[node_s][node_t]
|
||||
num_v = min(flow_dict[node_s][node_v], flow_dict[node_v][node_t])
|
||||
if num == 0:
|
||||
pass
|
||||
else:
|
||||
result_dict[node_v] = result_dict[node_v] + num_v / num
|
||||
return result_dict
|
||||
|
||||
|
||||
# flow betweenness
|
||||
def NumberOfFlow(G):
|
||||
nodes = G.nodes
|
||||
result_dict = dict()
|
||||
for node1, _ in nodes.items():
|
||||
result_dict[node1] = dict()
|
||||
for node2, _ in nodes.items():
|
||||
if node1 == node2:
|
||||
pass
|
||||
else:
|
||||
result_dict[node1][node2] = edmonds_karp(G, node1, node2)
|
||||
return result_dict
|
||||
|
||||
|
||||
def edmonds_karp(G, source, sink):
|
||||
nodes = G.nodes
|
||||
parent = dict()
|
||||
for node, _ in nodes.items():
|
||||
parent[node] = -1
|
||||
|
||||
adj = copy.deepcopy(G.adj)
|
||||
max_flow = 0
|
||||
while bfs(G, source, sink, parent, adj):
|
||||
path_flow = float("inf")
|
||||
s = sink
|
||||
while s != source:
|
||||
path_flow = min(path_flow, adj[parent[s]][s].get("weight", 1))
|
||||
s = parent[s]
|
||||
max_flow += path_flow
|
||||
v = sink
|
||||
while v != source:
|
||||
u = parent[v]
|
||||
x = adj[u][v].get("weight", 1)
|
||||
adj[u][v].update({"weight": x})
|
||||
adj[u][v]["weight"] -= path_flow
|
||||
|
||||
flag = 0
|
||||
if v not in adj:
|
||||
adj[v] = dict()
|
||||
if u not in adj[v]:
|
||||
adj[v][u] = dict()
|
||||
flag = 1
|
||||
if flag == 1:
|
||||
x = 0
|
||||
else:
|
||||
x = adj[v][u].get("weight", 1)
|
||||
adj[v][u].update({"weight": x})
|
||||
adj[v][u]["weight"] += path_flow
|
||||
v = parent[v]
|
||||
return max_flow
|
||||
|
||||
|
||||
def bfs(G, source, sink, parent, adj):
|
||||
nodes = G.nodes
|
||||
visited = dict()
|
||||
for node, _ in nodes.items():
|
||||
visited[node] = 0
|
||||
queue = collections.deque()
|
||||
queue.append(source)
|
||||
visited[source] = True
|
||||
while queue:
|
||||
u = queue.popleft()
|
||||
if u not in adj:
|
||||
continue
|
||||
for v, attr in adj[u].items():
|
||||
if (visited[v] == False) and (attr.get("weight", 1) > 0):
|
||||
queue.append(v)
|
||||
visited[v] = True
|
||||
parent[v] = u
|
||||
return visited[sink]
|
||||
@@ -0,0 +1,105 @@
|
||||
from easygraph.utils import *
|
||||
import numpy as np
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
__all__ = ["katz_centrality"]
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_katz_centrality")
|
||||
def katz_centrality(G, alpha=0.1, beta=1.0, max_iter=1000, tol=1e-6, normalized=True):
|
||||
r"""
|
||||
Compute the Katz centrality for nodes in a graph.
|
||||
|
||||
Katz centrality computes the influence of a node based on the total number
|
||||
of walks between nodes, attenuated by a factor of their length. It is
|
||||
defined as the solution to the linear system:
|
||||
|
||||
.. math::
|
||||
|
||||
x = \alpha A x + \beta
|
||||
|
||||
where:
|
||||
- \( A \) is the adjacency matrix of the graph,
|
||||
- \( \alpha \) is a scalar attenuation factor,
|
||||
- \( \beta \) is the bias vector (typically all ones),
|
||||
- and \( x \) is the resulting centrality vector.
|
||||
|
||||
The algorithm runs an iterative fixed-point method until convergence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
An EasyGraph graph instance. Must be simple (non-multigraph).
|
||||
|
||||
alpha : float, optional (default=0.1)
|
||||
Attenuation factor, must be smaller than the reciprocal of the largest
|
||||
eigenvalue of the adjacency matrix to ensure convergence.
|
||||
|
||||
beta : float or dict, optional (default=1.0)
|
||||
Bias term. Can be a constant scalar applied to all nodes, or a dictionary
|
||||
mapping node IDs to values.
|
||||
|
||||
max_iter : int, optional (default=1000)
|
||||
Maximum number of iterations before the algorithm terminates.
|
||||
|
||||
tol : float, optional (default=1e-6)
|
||||
Convergence tolerance. Iteration stops when the L1 norm of the difference
|
||||
between successive iterations is below this threshold.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True, the result vector will be normalized to unit norm (L2).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
A dictionary mapping node IDs to Katz centrality scores.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If the algorithm fails to converge within `max_iter` iterations.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import easygraph as eg
|
||||
>>> from easygraph import katz_centrality
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
>>> katz_centrality(G, alpha=0.05)
|
||||
{0: 0.370..., 1: 0.447..., 2: 0.447..., 3: 0.370...}
|
||||
"""
|
||||
# Create node ordering
|
||||
nodes = list(G.nodes)
|
||||
n = len(nodes)
|
||||
node_to_index = {node: i for i, node in enumerate(nodes)}
|
||||
index_to_node = {i: node for i, node in enumerate(nodes)}
|
||||
|
||||
# Build adjacency matrix
|
||||
A = np.zeros((n, n), dtype=np.float64)
|
||||
for u in G.nodes:
|
||||
for v in G.adj[u]:
|
||||
A[node_to_index[u], node_to_index[v]] = 1.0
|
||||
|
||||
# Initialize x and beta
|
||||
x = np.ones(n, dtype=np.float64)
|
||||
if isinstance(beta, dict):
|
||||
b = np.array([beta.get(index_to_node[i], 1.0) for i in range(n)])
|
||||
else:
|
||||
b = np.ones(n, dtype=np.float64) * beta
|
||||
|
||||
# Iterative update using vectorized ops
|
||||
for _ in range(max_iter):
|
||||
x_new = alpha * A @ x + b
|
||||
if np.linalg.norm(x_new - x, ord=1) < tol:
|
||||
break
|
||||
x = x_new
|
||||
else:
|
||||
raise RuntimeError(f"Katz centrality failed to converge in {max_iter} iterations")
|
||||
|
||||
if normalized:
|
||||
norm = np.linalg.norm(x)
|
||||
if norm > 0:
|
||||
x /= norm
|
||||
|
||||
result = {index_to_node[i]: float(x[i]) for i in range(n)}
|
||||
return result
|
||||
@@ -0,0 +1,134 @@
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = ["laplacian"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def laplacian(G, n_workers=None):
|
||||
"""Returns the laplacian centrality of each node in the weighted graph
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
weighted graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
CL : dict
|
||||
the laplacian centrality of each node in the weighted graph
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns the laplacian centrality of each node in the weighted graph G
|
||||
|
||||
>>> laplacian(G)
|
||||
|
||||
Reference
|
||||
---------
|
||||
.. [1] Xingqin Qi, Eddie Fuller, Qin Wu, Yezhou Wu, Cun-Quan Zhang.
|
||||
"Laplacian centrality: A new centrality measure for weighted networks."
|
||||
Information Sciences, Volume 194, Pages 240-253, 2012.
|
||||
|
||||
"""
|
||||
adj = G.adj
|
||||
from collections import defaultdict
|
||||
|
||||
X = defaultdict(int)
|
||||
W = defaultdict(int)
|
||||
CL = {}
|
||||
|
||||
if n_workers is not None:
|
||||
# use the parallel version for large graph
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
nodes = list(G.nodes)
|
||||
random.shuffle(nodes)
|
||||
|
||||
if len(nodes) > n_workers * 30000:
|
||||
nodes = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes = split(nodes, n_workers)
|
||||
|
||||
local_function = partial(initialize_parallel, G=G, adj=adj)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(local_function, nodes)
|
||||
resX, resW = [], []
|
||||
for i in ret:
|
||||
for x in i:
|
||||
resX.append(x[0])
|
||||
resW.append(x[1])
|
||||
X = dict(resX)
|
||||
W = dict(resW)
|
||||
ELG = sum(X[i] * X[i] for i in G) + sum(W[i] for i in G)
|
||||
local_function = partial(laplacian_parallel, G=G, X=X, W=W, adj=adj, ELG=ELG)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(local_function, nodes)
|
||||
res = [x for i in ret for x in i]
|
||||
CL = dict(res)
|
||||
|
||||
else:
|
||||
# use np-parallel version for small graph
|
||||
for i in G:
|
||||
for j in G:
|
||||
if i in G and j in G[i]:
|
||||
X[i] += adj[i][j].get("weight", 1)
|
||||
W[i] += adj[i][j].get("weight", 1) * adj[i][j].get("weight", 1)
|
||||
ELG = sum(X[i] * X[i] for i in G) + sum(W[i] for i in G)
|
||||
for i in G:
|
||||
import copy
|
||||
|
||||
Xi = copy.deepcopy(X)
|
||||
for j in G:
|
||||
if j in adj.keys() and i in adj[j].keys():
|
||||
Xi[j] -= adj[j][i].get("weight", 1)
|
||||
Xi[i] = 0
|
||||
ELGi = sum(Xi[i] * Xi[i] for i in G) + sum(W[i] for i in G) - 2 * W[i]
|
||||
if ELG:
|
||||
CL[i] = (float)(ELG - ELGi) / ELG
|
||||
return CL
|
||||
|
||||
|
||||
def initialize_parallel(nodes, G, adj):
|
||||
ret = []
|
||||
for i in nodes:
|
||||
X = 0
|
||||
W = 0
|
||||
for j in G:
|
||||
if j in G[i]:
|
||||
X += adj[i][j].get("weight", 1)
|
||||
W += adj[i][j].get("weight", 1) * adj[i][j].get("weight", 1)
|
||||
ret.append([[i, X], [i, W]])
|
||||
return ret
|
||||
|
||||
|
||||
def laplacian_parallel(nodes, G, X, W, adj, ELG):
|
||||
ret = []
|
||||
for i in nodes:
|
||||
import copy
|
||||
|
||||
Xi = copy.deepcopy(X)
|
||||
for j in G:
|
||||
if j in adj.keys() and i in adj[j].keys():
|
||||
Xi[j] -= adj[j][i].get("weight", 1)
|
||||
Xi[i] = 0
|
||||
ELGi = sum(Xi[i] * Xi[i] for i in G) + sum(W[i] for i in G) - 2 * W[i]
|
||||
if ELG:
|
||||
ret.append([i, (float)(ELG - ELGi) / ELG])
|
||||
return ret
|
||||
|
||||
|
||||
def sort(data):
|
||||
return dict(sorted(data.items(), key=lambda x: x[0], reverse=True))
|
||||
|
||||
|
||||
def output(data, path):
|
||||
import json
|
||||
|
||||
data = sort(data)
|
||||
json_str = json.dumps(data, ensure_ascii=False, indent=4)
|
||||
with open(path, "w", encoding="utf-8") as json_file:
|
||||
json_file.write(json_str)
|
||||
@@ -0,0 +1,58 @@
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = ["pagerank"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_pagerank")
|
||||
def pagerank(G, alpha=0.85, weight=None):
|
||||
"""
|
||||
Returns the PageRank value of each node in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
Undirected graph will be considered as directed graph with two directed edges for each undirected edge.
|
||||
|
||||
alpha : float
|
||||
The damping factor. Default is 0.85
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
if len(G) == 0:
|
||||
return {}
|
||||
M = google_matrix(G, alpha=alpha, weight=weight)
|
||||
|
||||
# use numpy LAPACK solver
|
||||
eigenvalues, eigenvectors = np.linalg.eig(M.T)
|
||||
ind = np.argmax(eigenvalues)
|
||||
# eigenvector of largest eigenvalue is at ind, normalized
|
||||
largest = np.array(eigenvectors[:, ind]).flatten().real
|
||||
norm = float(largest.sum())
|
||||
return dict(zip(G, map(float, largest / norm)))
|
||||
|
||||
|
||||
def google_matrix(G, alpha, weight=None):
|
||||
import numpy as np
|
||||
|
||||
M = eg.to_numpy_array(G, weight=weight).astype(float)
|
||||
N = len(G)
|
||||
if N == 0:
|
||||
return M
|
||||
|
||||
# Get dangling nodes(nodes with no out link)
|
||||
dangling_nodes = np.where(M.sum(axis=1) == 0)[0]
|
||||
dangling_weights = np.repeat(1.0 / N, N)
|
||||
for node in dangling_nodes:
|
||||
M[node] = dangling_weights
|
||||
|
||||
M /= M.sum(axis=1)[:, np.newaxis]
|
||||
|
||||
return alpha * M + (1 - alpha) * np.repeat(1.0 / N, N)
|
||||
@@ -0,0 +1,99 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class Test_betweenness(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
self.undirected = eg.Graph()
|
||||
self.undirected.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
|
||||
self.directed = eg.DiGraph()
|
||||
self.directed.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
|
||||
self.disconnected = eg.Graph()
|
||||
self.disconnected.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.single_node = eg.Graph()
|
||||
self.single_node.add_node(42)
|
||||
|
||||
self.two_node = eg.Graph()
|
||||
self.two_node.add_edge("A", "B")
|
||||
|
||||
self.named_nodes = eg.Graph()
|
||||
self.named_nodes.add_edges_from([("X", "Y"), ("Y", "Z")])
|
||||
|
||||
def test_betweenness(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.functions.betweenness_centrality(i))
|
||||
|
||||
def test_basic_undirected(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected)
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
self.assertTrue(all(isinstance(x, float) for x in result))
|
||||
|
||||
def test_basic_directed(self):
|
||||
result = eg.functions.betweenness_centrality(self.directed)
|
||||
self.assertEqual(len(result), len(self.directed.nodes))
|
||||
|
||||
def test_disconnected(self):
|
||||
result = eg.functions.betweenness_centrality(self.disconnected)
|
||||
self.assertEqual(len(result), len(self.disconnected.nodes))
|
||||
self.assertTrue(all(v == 0.0 for v in result))
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = eg.functions.betweenness_centrality(self.single_node)
|
||||
self.assertEqual(result, [0.0])
|
||||
|
||||
def test_two_node_graph(self):
|
||||
result = eg.functions.betweenness_centrality(self.two_node)
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertTrue(all(v == 0.0 for v in result))
|
||||
|
||||
def test_named_nodes_graph(self):
|
||||
result = eg.functions.betweenness_centrality(self.named_nodes)
|
||||
self.assertEqual(len(result), 3)
|
||||
|
||||
def test_with_endpoints(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected, endpoints=True)
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
|
||||
def test_unormalized(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected, normalized=False)
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
|
||||
def test_subset_sources(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected, sources=[1, 2])
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
|
||||
def test_parallel_workers(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected, n_workers=2)
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
|
||||
def test_multigraph_error(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edges_from([(0, 1), (0, 1)])
|
||||
with self.assertRaises(eg.EasyGraphNotImplemented):
|
||||
eg.functions.betweenness_centrality(G)
|
||||
|
||||
def test_all_nodes_type_mix(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), ("A", "B"), ((1, 2), (3, 4))])
|
||||
result = eg.functions.betweenness_centrality(G)
|
||||
self.assertEqual(len(result), len(G.nodes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,86 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.classes.multigraph import MultiGraph
|
||||
from easygraph.functions.centrality import closeness_centrality
|
||||
|
||||
|
||||
class Test_closeness(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
self.simple_graph = eg.Graph()
|
||||
self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
self.weighted_graph = eg.Graph()
|
||||
self.weighted_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
for u, v, data in self.weighted_graph.edges:
|
||||
data["weight"] = 2
|
||||
|
||||
self.disconnected_graph = eg.Graph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(42)
|
||||
|
||||
self.mixed_nodes_graph = eg.Graph()
|
||||
self.mixed_nodes_graph.add_edges_from([(1, 2), ("X", "Y"), ((1, 2), (3, 4))])
|
||||
|
||||
def test_closeness(self):
|
||||
for i in self.test_graphs:
|
||||
result = closeness_centrality(i)
|
||||
self.assertEqual(len(result), len(i))
|
||||
|
||||
def test_simple_graph(self):
|
||||
result = closeness_centrality(self.simple_graph)
|
||||
self.assertEqual(len(result), len(self.simple_graph))
|
||||
self.assertTrue(all(isinstance(x, float) for x in result))
|
||||
|
||||
def test_directed_graph(self):
|
||||
result = closeness_centrality(self.directed_graph)
|
||||
self.assertEqual(len(result), len(self.directed_graph))
|
||||
|
||||
def test_weighted_graph(self):
|
||||
result = closeness_centrality(self.weighted_graph, weight="weight")
|
||||
self.assertEqual(len(result), len(self.weighted_graph))
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
result = closeness_centrality(self.disconnected_graph)
|
||||
self.assertEqual(len(result), len(self.disconnected_graph))
|
||||
self.assertTrue(all(v <= 1.0 for v in result))
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = closeness_centrality(self.single_node_graph)
|
||||
self.assertEqual(result, [0.0])
|
||||
|
||||
def test_mixed_node_types(self):
|
||||
result = closeness_centrality(self.mixed_nodes_graph)
|
||||
self.assertEqual(len(result), len(self.mixed_nodes_graph))
|
||||
|
||||
def test_parallel_workers(self):
|
||||
result = closeness_centrality(self.simple_graph, n_workers=2)
|
||||
self.assertEqual(len(result), len(self.simple_graph))
|
||||
|
||||
def test_multigraph_raises(self):
|
||||
G = MultiGraph()
|
||||
G.add_edges_from([(0, 1), (0, 1)])
|
||||
with self.assertRaises(eg.EasyGraphNotImplemented):
|
||||
closeness_centrality(G)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,78 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_degree(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
self.undirected_graph = eg.Graph()
|
||||
self.undirected_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
# Directed graph
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
# Single-node graph
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(0)
|
||||
|
||||
# Empty graph
|
||||
self.empty_graph = eg.Graph()
|
||||
|
||||
# Multigraph
|
||||
self.multigraph = eg.MultiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)])
|
||||
|
||||
def test_degree(self):
|
||||
for i in self.test_graphs:
|
||||
print(i.edges)
|
||||
print(eg.functions.degree_centrality(i))
|
||||
print(eg.functions.in_degree_centrality(i))
|
||||
print(eg.functions.out_degree_centrality(i))
|
||||
|
||||
def test_degree_centrality_undirected(self):
|
||||
result = eg.functions.degree_centrality(self.undirected_graph)
|
||||
self.assertEqual(len(result), len(self.undirected_graph))
|
||||
self.assertTrue(all(isinstance(v, float) for v in result.values()))
|
||||
|
||||
def test_degree_centrality_directed(self):
|
||||
result = eg.functions.degree_centrality(self.directed_graph)
|
||||
self.assertEqual(len(result), len(self.directed_graph))
|
||||
|
||||
def test_degree_centrality_single_node(self):
|
||||
result = eg.functions.degree_centrality(self.single_node_graph)
|
||||
self.assertEqual(result, {0: 1})
|
||||
|
||||
def test_degree_centrality_empty_graph(self):
|
||||
result = eg.functions.degree_centrality(self.empty_graph)
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_in_out_degree_centrality_directed(self):
|
||||
in_deg = eg.functions.in_degree_centrality(self.directed_graph)
|
||||
out_deg = eg.functions.out_degree_centrality(self.directed_graph)
|
||||
self.assertEqual(len(in_deg), len(self.directed_graph))
|
||||
self.assertEqual(len(out_deg), len(self.directed_graph))
|
||||
|
||||
def test_in_out_degree_centrality_single_node(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_node(1)
|
||||
self.assertEqual(eg.functions.in_degree_centrality(G), {1: 1})
|
||||
self.assertEqual(eg.functions.out_degree_centrality(G), {1: 1})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,73 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_egobetweenness(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
print(self.test_graphs[-1].edges)
|
||||
|
||||
self.graph = eg.Graph()
|
||||
self.graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
|
||||
self.mixed_nodes_graph = eg.Graph()
|
||||
self.mixed_nodes_graph.add_edges_from([(1, "A"), ("A", (2, 3)), ((2, 3), "B")])
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(42)
|
||||
|
||||
self.disconnected_graph = eg.Graph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) # two components
|
||||
|
||||
self.multigraph = eg.MultiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)]) # parallel edges
|
||||
|
||||
def test_egobetweenness(self):
|
||||
print(eg.functions.ego_betweenness(self.test_graphs[-1], 4))
|
||||
|
||||
def test_small_undirected_graph(self):
|
||||
result = eg.functions.ego_betweenness(self.graph, 1)
|
||||
self.assertIsInstance(result, float)
|
||||
self.assertGreaterEqual(result, 0)
|
||||
|
||||
def test_directed_graph(self):
|
||||
result = eg.functions.ego_betweenness(self.directed_graph, 0)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_mixed_node_types(self):
|
||||
result = eg.functions.ego_betweenness(self.mixed_nodes_graph, "A")
|
||||
self.assertIsInstance(result, float)
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = eg.functions.ego_betweenness(self.single_node_graph, 42)
|
||||
self.assertEqual(result, 0.0)
|
||||
|
||||
def test_disconnected_graph_component(self):
|
||||
result_0 = eg.functions.ego_betweenness(self.disconnected_graph, 0)
|
||||
result_2 = eg.functions.ego_betweenness(self.disconnected_graph, 2)
|
||||
self.assertIsInstance(result_0, float)
|
||||
self.assertIsInstance(result_2, float)
|
||||
|
||||
def test_raises_on_multigraph(self):
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
eg.functions.ego_betweenness(self.multigraph, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,90 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_flowbetweenness(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
("String", "Bool"),
|
||||
(2, 1),
|
||||
(0, 0),
|
||||
(-99, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from(
|
||||
[
|
||||
(0, 1, {"weight": 3}),
|
||||
(1, 2, {"weight": 1}),
|
||||
(0, 2, {"weight": 1}),
|
||||
(2, 3, {"weight": 2}),
|
||||
(1, 3, {"weight": 4}),
|
||||
]
|
||||
)
|
||||
|
||||
self.graph_with_self_loop = eg.DiGraph()
|
||||
self.graph_with_self_loop.add_edges_from([(0, 1), (1, 2), (2, 2), (2, 3)])
|
||||
|
||||
self.disconnected_graph = eg.DiGraph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.undirected_graph = eg.Graph()
|
||||
self.undirected_graph.add_edges_from([(0, 1), (1, 2)])
|
||||
|
||||
self.single_node_graph = eg.DiGraph()
|
||||
self.single_node_graph.add_node(0)
|
||||
|
||||
self.mixed_type_graph = eg.DiGraph()
|
||||
self.mixed_type_graph.add_edges_from([(1, "A"), ("A", (2, 3)), ((2, 3), "B")])
|
||||
|
||||
self.multigraph = eg.MultiDiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)])
|
||||
|
||||
def test_flowbetweenness_centrality(self):
|
||||
for i in self.test_graphs:
|
||||
print(i.edges)
|
||||
print(eg.functions.flowbetweenness_centrality(i))
|
||||
|
||||
def test_flowbetweenness_on_directed(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.directed_graph)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertTrue(
|
||||
all(isinstance(v, float) or isinstance(v, int) for v in result.values())
|
||||
)
|
||||
|
||||
def test_flowbetweenness_on_self_loop(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.graph_with_self_loop)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
def test_flowbetweenness_on_disconnected(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.disconnected_graph)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
def test_flowbetweenness_on_single_node(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.single_node_graph)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result, {0: 0})
|
||||
|
||||
def test_flowbetweenness_on_mixed_types(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.mixed_type_graph)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
def test_flowbetweenness_on_undirected_warns(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.undirected_graph)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_flowbetweenness_raises_on_multigraph(self):
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
eg.functions.flowbetweenness_centrality(self.multigraph)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,106 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_laplacian(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
("String", "Bool"),
|
||||
(2, 1),
|
||||
(0, 0),
|
||||
(-99, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.weighted_graph = eg.Graph()
|
||||
self.weighted_graph.add_edges_from(
|
||||
[
|
||||
(0, 1, {"weight": 2}),
|
||||
(1, 2, {"weight": 3}),
|
||||
(2, 3, {"weight": 4}),
|
||||
(3, 0, {"weight": 1}),
|
||||
]
|
||||
)
|
||||
|
||||
self.unweighted_graph = eg.Graph()
|
||||
self.unweighted_graph.add_edges_from(
|
||||
[
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
]
|
||||
)
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from(
|
||||
[
|
||||
(0, 1, {"weight": 2}),
|
||||
(1, 2, {"weight": 1}),
|
||||
(2, 0, {"weight": 3}),
|
||||
]
|
||||
)
|
||||
|
||||
self.self_loop_graph = eg.Graph()
|
||||
self.self_loop_graph.add_edges_from(
|
||||
[
|
||||
(0, 0, {"weight": 2}),
|
||||
(0, 1, {"weight": 1}),
|
||||
]
|
||||
)
|
||||
|
||||
self.mixed_type_graph = eg.Graph()
|
||||
self.mixed_type_graph.add_edges_from(
|
||||
[
|
||||
("A", "B"),
|
||||
("B", (1, 2)),
|
||||
]
|
||||
)
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(42)
|
||||
|
||||
self.multigraph = eg.MultiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)])
|
||||
|
||||
def test_laplacian(self):
|
||||
for i in self.test_graphs:
|
||||
print(i.edges)
|
||||
print(eg.functions.laplacian(i))
|
||||
|
||||
def test_weighted_graph(self):
|
||||
result = eg.functions.laplacian(self.weighted_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.weighted_graph.nodes))
|
||||
|
||||
def test_unweighted_graph(self):
|
||||
result = eg.functions.laplacian(self.unweighted_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.unweighted_graph.nodes))
|
||||
|
||||
def test_directed_graph(self):
|
||||
result = eg.functions.laplacian(self.directed_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.directed_graph.nodes))
|
||||
|
||||
def test_self_loop_graph(self):
|
||||
result = eg.functions.laplacian(self.self_loop_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.self_loop_graph.nodes))
|
||||
|
||||
def test_mixed_node_types(self):
|
||||
result = eg.functions.laplacian(self.mixed_type_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.mixed_type_graph.nodes))
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = eg.functions.laplacian(self.single_node_graph)
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_multigraph_raises(self):
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
eg.functions.laplacian(self.multigraph)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user