chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:36:30 +08:00
commit 55ab4e4a73
473 changed files with 72932 additions and 0 deletions
+29
View File
@@ -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!")
+15
View File
@@ -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/"
+19
View File
@@ -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"
)
+996
View File
@@ -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
+420
View File
@@ -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
+15
View File
@@ -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
+729
View File
@@ -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)
+447
View File
@@ -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()
View File
+145
View File
@@ -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)
+112
View File
@@ -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))
+122
View File
@@ -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()
+149
View File
@@ -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)
+131
View File
@@ -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]
+591
View File
@@ -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
+29
View File
@@ -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",
# ]
+106
View File
@@ -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
+90
View File
@@ -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
+74
View File
@@ -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
+35
View File
@@ -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
+110
View File
@@ -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
+106
View File
@@ -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
+875
View File
@@ -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
+118
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
from .email_enron import *
from .email_eu import *
from .hospital_lyon import *
from .load_dataset import *
+86
View File
@@ -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
+81
View File
@@ -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
+133
View File
@@ -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"]
)
+109
View File
@@ -0,0 +1,109 @@
"""Facebook Ego-Net Dataset
This dataset contains a subset of Facebooks 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)
+129
View File
@@ -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)
+210
View File
@@ -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
+125
View File
@@ -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
+216
View File
@@ -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
+318
View File
@@ -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
+82
View File
@@ -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],
},
}
+10
View File
@@ -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 *
+14
View File
@@ -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],
},
}
+279
View File
@@ -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)
+93
View File
@@ -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
+217
View File
@@ -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"]
+104
View File
@@ -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
+107
View File
@@ -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.
+65
View File
@@ -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
+358
View File
@@ -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
+118
View File
@@ -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)
+105
View File
@@ -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
+84
View File
@@ -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"""
+10
View File
@@ -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"
)
+204
View File
@@ -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
+21
View File
@@ -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"
)
+4
View File
@@ -0,0 +1,4 @@
from .avg_degree import *
from .cluster import *
from .localassort import *
from .predecessor_path_based import *
+31
View File
@@ -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
+559
View File
@@ -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
+226
View File
@@ -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
+105
View File
@@ -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
+125
View File
@@ -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
+134
View File
@@ -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