chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
from .directed_graph import DiGraph
|
||||
from .directed_graph import DiGraphC
|
||||
from .directed_multigraph import MultiDiGraph
|
||||
from .graph import Graph
|
||||
from .graph import GraphC
|
||||
from .graphviews import *
|
||||
from .multigraph import MultiGraph
|
||||
from .operation import *
|
||||
|
||||
|
||||
try:
|
||||
from .base import BaseHypergraph
|
||||
from .base import load_structure
|
||||
from .hypergraph import Hypergraph
|
||||
except:
|
||||
print(
|
||||
"Warning raise in module:classes. Please install Pytorch before you use"
|
||||
" functions related to Hypergraph"
|
||||
)
|
||||
@@ -0,0 +1,996 @@
|
||||
import abc
|
||||
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["load_structure", "BaseHypergraph"]
|
||||
|
||||
|
||||
def load_structure(file_path: Union[str, Path]):
|
||||
r"""Load a EasyGraph's high-order network structure from a file. The supported structure ``Hypergraph``.
|
||||
|
||||
Args:
|
||||
``file_path`` (``Union[str, Path]``): The file path to load the EasyGraph's structure.
|
||||
"""
|
||||
import pickle as pkl
|
||||
|
||||
import easygraph
|
||||
|
||||
file_path = Path(file_path)
|
||||
assert file_path.exists(), f"{file_path} does not exist"
|
||||
with open(file_path, "rb") as f:
|
||||
data = pkl.load(f)
|
||||
class_name, state_dict = data["class"], data["state_dict"]
|
||||
structure_class = getattr(easygraph, class_name)
|
||||
structure = structure_class.from_state_dict(state_dict)
|
||||
return structure
|
||||
|
||||
|
||||
class BaseHypergraph:
|
||||
r"""The ``BaseHypergraph`` class is the base class for all hypergraph structures.
|
||||
|
||||
Args:
|
||||
``num_v`` (``int``): The number of vertices.
|
||||
``e_list`` (``Union[List[int], List[List[int]]], optional``): Edge list. Defaults to ``None``.
|
||||
``e_weight`` (``Union[float, List[float]], optional``): A list of weights for edges. Defaults to ``None``.
|
||||
``extra_selfloop`` (``bool``, optional): Whether to add extra self-loop to the graph. Defaults to ``False``.
|
||||
``device`` (``torch.device``, optional): The device to store the graph. Defaults to ``torch.device('cpu')``.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_v: int,
|
||||
v_property: Optional[Union[Dict, List[Dict]]] = None,
|
||||
e_list: Optional[Union[List[int], List[List[int]]]] = None,
|
||||
e_property: Optional[Union[Dict, List[Dict]]] = None,
|
||||
e_weight: Optional[Union[float, List[float]]] = None,
|
||||
extra_selfloop: bool = False,
|
||||
device: str = "cpu",
|
||||
):
|
||||
assert (
|
||||
isinstance(num_v, int) and num_v > 0
|
||||
), "num_v should be a positive integer"
|
||||
self.clear()
|
||||
self._num_v = num_v
|
||||
# self.device = torch.cuda.device(device)
|
||||
if v_property == None:
|
||||
self._v_property = [{} for i in range(num_v)]
|
||||
else:
|
||||
v_property = self._format_v_property_list(num_v, v_property)
|
||||
self._v_property = v_property
|
||||
|
||||
if e_property == None and e_list != None:
|
||||
self._e_property = [{} for i in range(len(e_list))]
|
||||
elif e_property != None and e_list != None:
|
||||
e_property = self._format_e_property_list(len(e_list), e_property)
|
||||
self._e_property = e_property
|
||||
|
||||
self._has_extra_selfloop = extra_selfloop
|
||||
|
||||
@abc.abstractmethod
|
||||
def __repr__(self) -> str:
|
||||
r"""Print the hypergraph information."""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def state_dict(self) -> Dict[str, Any]:
|
||||
r"""Get the state dict of the hypergraph."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def save(self, file_path: Union[str, Path]):
|
||||
r"""Save the EasyGraph's hypergraph structure to a file.
|
||||
|
||||
Args:
|
||||
``file_path`` (``str``): The file_path to store the EasyGraph's hypergraph structure.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def load(file_path: Union[str, Path]):
|
||||
r"""Load the EasyGraph's hypergraph structure from a file.
|
||||
|
||||
Args:
|
||||
``file_path`` (``str``): The file path to load the DEasyGraph's hypergraph structure.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def from_state_dict(state_dict: dict):
|
||||
r"""Load the EasyGraph's hypergraph structure from the state dict.
|
||||
|
||||
Args:
|
||||
``state_dict`` (``dict``): The state dict to load the EasyGraph's hypergraph.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def draw(self, **kwargs):
|
||||
r"""Draw the structure."""
|
||||
|
||||
def clear(self):
|
||||
r"""Remove all hyperedges and caches from the hypergraph."""
|
||||
self._clear_raw()
|
||||
self._clear_cache()
|
||||
|
||||
def _clear_raw(self):
|
||||
self._v_weight = None
|
||||
self._raw_groups = {}
|
||||
|
||||
def _clear_cache(self, group_name: Optional[str] = None):
|
||||
r"""Clear the cache."""
|
||||
self.cache = {}
|
||||
if group_name is None:
|
||||
self.group_cache = defaultdict(dict)
|
||||
else:
|
||||
self.group_cache.pop(group_name, None)
|
||||
|
||||
@abc.abstractmethod
|
||||
def clone(self) -> "BaseHypergraph":
|
||||
r"""Return a copy of this type of hypergraph."""
|
||||
|
||||
def to(self, device: str = "cpu") -> "BaseHypergraph":
|
||||
r"""Move the hypergraph to the specified device.
|
||||
|
||||
Args:
|
||||
``device`` (``torch.device``): The device to store the hypergraph.
|
||||
"""
|
||||
# self.device = torch.device
|
||||
for v in self.vars_for_DL:
|
||||
if v in self.cache and self.cache[v] is not None:
|
||||
self.cache[v] = self.cache[v].to(device)
|
||||
for name in self.group_names:
|
||||
if (
|
||||
v in self.group_cache[name]
|
||||
and self.group_cache[name][v] is not None
|
||||
):
|
||||
self.group_cache[name][v] = self.group_cache[name][v].to(device)
|
||||
return self
|
||||
|
||||
# utils
|
||||
def _hyperedge_code(self, src_v_set: List[int], dst_v_set: List[int]) -> Tuple:
|
||||
r"""Generate the hyperedge code.
|
||||
|
||||
Args:
|
||||
``src_v_set`` (``List[int]``): The source vertex set.
|
||||
``dst_v_set`` (``List[int]``): The destination vertex set.
|
||||
"""
|
||||
return tuple([src_v_set, dst_v_set])
|
||||
|
||||
def _merge_hyperedges(self, e1: dict, e2: dict, op: str = "mean"):
|
||||
assert op in [
|
||||
"mean",
|
||||
"sum",
|
||||
"max",
|
||||
], "Hyperedge merge operation must be one of ['mean', 'sum', 'max']"
|
||||
_func = {
|
||||
"mean": lambda x, y: (x + y) / 2,
|
||||
"sum": lambda x, y: x + y,
|
||||
"max": lambda x, y: max(x, y),
|
||||
}
|
||||
_e = {}
|
||||
if "w_v2e" in e1 and "w_v2e" in e2:
|
||||
for _idx in range(len(e1["w_v2e"])):
|
||||
_e["w_v2e"] = _func[op](e1["w_v2e"][_idx], e2["w_v2e"][_idx])
|
||||
if "w_e2v" in e1 and "w_e2v" in e2:
|
||||
for _idx in range(len(e1["w_e2v"])):
|
||||
_e["w_e2v"] = _func[op](e1["w_e2v"][_idx], e2["w_e2v"][_idx])
|
||||
_e["w_e"] = _func[op](e1["w_e"], e2["w_e"])
|
||||
return _e
|
||||
|
||||
@staticmethod
|
||||
def _format_e_list(e_list: Union[List[int], List[List[int]]]) -> List[List[int]]:
|
||||
r"""Format the hyperedge list.
|
||||
|
||||
Args:
|
||||
``e_list`` (``List[int]`` or ``List[List[int]]``): The hyperedge list.
|
||||
"""
|
||||
if len(e_list) == 0:
|
||||
pass
|
||||
elif type(e_list[0]) in (int, float):
|
||||
return [tuple(sorted(e_list))]
|
||||
elif type(e_list) == tuple:
|
||||
e_list = list(e_list)
|
||||
elif type(e_list) == list:
|
||||
pass
|
||||
else:
|
||||
raise TypeError("e_list must be List[int] or List[List[int]].")
|
||||
for _idx in range(len(e_list)):
|
||||
e_list[_idx] = tuple(sorted(list(set(e_list[_idx]))))
|
||||
|
||||
return e_list
|
||||
|
||||
def _format_e_property_list(self, e_num, e_property_list: Union[Dict, List[Dict]]):
|
||||
r"""Format the property list.
|
||||
|
||||
Args:
|
||||
``e_list`` (``Dict`` or ``List[Dict]``): The property list.
|
||||
"""
|
||||
if type(e_property_list) == dict:
|
||||
return [e_property_list]
|
||||
elif type(e_property_list) == list and len(e_property_list) != e_num:
|
||||
raise EasyGraphError(
|
||||
"The length of property list must be equal to edge number"
|
||||
)
|
||||
elif type(e_property_list) == list:
|
||||
pass
|
||||
else:
|
||||
raise TypeError("e_property_list must be Dict or List[Dict].")
|
||||
|
||||
return e_property_list
|
||||
|
||||
def _format_v_property_list(self, v_num, v_property_list: Union[Dict, List[Dict]]):
|
||||
r"""Format the property list.
|
||||
|
||||
Args:
|
||||
``e_list`` (``Dict`` or ``List[Dict]``): The property list.
|
||||
"""
|
||||
if type(v_property_list) == dict:
|
||||
return [v_property_list]
|
||||
elif type(v_property_list) == list and len(v_property_list) != v_num:
|
||||
raise EasyGraphError(
|
||||
"The length of property list must be equal to node number"
|
||||
)
|
||||
elif type(v_property_list) == list:
|
||||
pass
|
||||
else:
|
||||
raise TypeError("v_property_list must be Dict or List[Dict].")
|
||||
|
||||
return v_property_list
|
||||
|
||||
@staticmethod
|
||||
def _format_e_list_and_w_on_them(
|
||||
e_list: Union[List[int], List[List[int]]],
|
||||
w_list: Optional[Union[List[int], List[List[int]]]] = None,
|
||||
):
|
||||
r"""Format ``e_list`` and ``w_list``.
|
||||
|
||||
Args:
|
||||
``e_list`` (Union[List[int], List[List[int]]]): Hyperedge list.
|
||||
``w_list`` (Optional[Union[List[int], List[List[int]]]]): Weights on connections. Defaults to ``None``.
|
||||
"""
|
||||
bad_connection_msg = (
|
||||
"The weight on connections between vertices and hyperedges must have the"
|
||||
" same size as the hyperedges."
|
||||
)
|
||||
if isinstance(e_list, tuple):
|
||||
e_list = list(e_list)
|
||||
if w_list is not None and isinstance(w_list, tuple):
|
||||
w_list = list(w_list)
|
||||
if isinstance(e_list[0], int) and w_list is None:
|
||||
w_list = [1] * len(e_list)
|
||||
e_list, w_list = [e_list], [w_list]
|
||||
elif isinstance(e_list[0], int) and w_list is not None:
|
||||
assert len(e_list) == len(w_list), bad_connection_msg
|
||||
e_list, w_list = [e_list], [w_list]
|
||||
elif isinstance(e_list[0], list) and w_list is None:
|
||||
w_list = [[1] * len(e) for e in e_list]
|
||||
assert len(e_list) == len(w_list), bad_connection_msg
|
||||
# TODO: this step can be speeded up
|
||||
for idx in range(len(e_list)):
|
||||
assert len(e_list[idx]) == len(w_list[idx]), bad_connection_msg
|
||||
cur_e, cur_w = np.array(e_list[idx]), np.array(w_list[idx])
|
||||
sorted_idx = np.argsort(cur_e)
|
||||
e_list[idx] = tuple(cur_e[sorted_idx].tolist())
|
||||
w_list[idx] = cur_w[sorted_idx].tolist()
|
||||
return e_list, w_list
|
||||
|
||||
def _fetch_H(self, direction: str, group_name: str):
|
||||
r"""Fetch the H matrix of the specified hyperedge group with ``torch.sparse_coo_tensor`` format.
|
||||
|
||||
Args:
|
||||
``direction`` (``str``): The direction of hyperedges can be either ``'v2e'`` or ``'e2v'``.
|
||||
``group_name`` (``str``): The name of the group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
assert direction in ["v2e", "e2v"], "direction must be one of ['v2e', 'e2v']"
|
||||
if direction == "v2e":
|
||||
select_idx = 0
|
||||
else:
|
||||
select_idx = 1
|
||||
num_e = len(self._raw_groups[group_name])
|
||||
e_idx, v_idx = [], []
|
||||
for _e_idx, e in enumerate(self._raw_groups[group_name].keys()):
|
||||
sub_e = e[select_idx]
|
||||
v_idx.extend(sub_e)
|
||||
e_idx.extend([_e_idx] * len(sub_e))
|
||||
|
||||
H = torch.sparse_coo_tensor(
|
||||
torch.tensor([v_idx, e_idx], dtype=torch.long),
|
||||
torch.ones(len(v_idx)),
|
||||
torch.Size([self.num_v, num_e]),
|
||||
device=self.device,
|
||||
).coalesce()
|
||||
return H
|
||||
|
||||
def _fetch_H_of_group(self, direction: str, group_name: str):
|
||||
r"""Fetch the H matrix of the specified hyperedge group with ``torch.sparse_coo_tensor`` format.
|
||||
|
||||
Args:
|
||||
``direction`` (``str``): The direction of hyperedges can be either ``'v2e'`` or ``'e2v'``.
|
||||
``group_name`` (``str``): The name of the group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
assert direction in ["v2e", "e2v"], "direction must be one of ['v2e', 'e2v']"
|
||||
if direction == "v2e":
|
||||
select_idx = 0
|
||||
else:
|
||||
select_idx = 1
|
||||
num_e = len(self._raw_groups[group_name])
|
||||
e_idx, v_idx = [], []
|
||||
for _e_idx, e in enumerate(self._raw_groups[group_name].keys()):
|
||||
sub_e = e[select_idx]
|
||||
v_idx.extend(sub_e)
|
||||
e_idx.extend([_e_idx] * len(sub_e))
|
||||
|
||||
H = torch.sparse_coo_tensor(
|
||||
torch.tensor([v_idx, e_idx], dtype=torch.long),
|
||||
torch.ones(len(v_idx)),
|
||||
torch.Size([self.num_v, num_e]),
|
||||
device=self.device,
|
||||
).coalesce()
|
||||
return H
|
||||
|
||||
def _fetch_R_of_group(self, direction: str, group_name: str):
|
||||
r"""Fetch the R matrix of the specified hyperedge group with ``torch.sparse_coo_tensor`` format.
|
||||
|
||||
Args:
|
||||
``direction`` (``str``): The direction of hyperedges can be either ``'v2e'`` or ``'e2v'``.
|
||||
``group_name`` (``str``): The name of the group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
assert direction in ["v2e", "e2v"], "direction must be one of ['v2e', 'e2v']"
|
||||
if direction == "v2e":
|
||||
select_idx = 0
|
||||
else:
|
||||
select_idx = 1
|
||||
num_e = len(self._raw_groups[group_name])
|
||||
e_idx, v_idx, w_list = [], [], []
|
||||
for _e_idx, e in enumerate(self._raw_groups[group_name].keys()):
|
||||
sub_e = e[select_idx]
|
||||
v_idx.extend(sub_e)
|
||||
e_idx.extend([_e_idx] * len(sub_e))
|
||||
w_list.extend(self._raw_groups[group_name][e][f"w_{direction}"])
|
||||
R = torch.sparse_coo_tensor(
|
||||
torch.vstack([v_idx, e_idx]),
|
||||
torch.tensor(w_list),
|
||||
torch.Size([self.num_v, num_e]),
|
||||
device=self.device,
|
||||
).coalesce()
|
||||
return R
|
||||
|
||||
def _fetch_W_of_group(self, group_name: str):
|
||||
r"""Fetch the W matrix of the specified hyperedge group with ``torch.sparse_coo_tensor`` format.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
w_list = [1.0] * len(self._raw_groups["main"])
|
||||
W = torch.tensor(w_list, device=self.device).view((-1, 1))
|
||||
return W
|
||||
|
||||
# some structure modification functions
|
||||
def add_hyperedges(
|
||||
self,
|
||||
e_list_v2e: Union[List[int], List[List[int]]],
|
||||
e_list_e2v: Union[List[int], List[List[int]]],
|
||||
w_list_v2e: Optional[Union[List[float], List[List[float]]]] = None,
|
||||
w_list_e2v: Optional[Union[List[float], List[List[float]]]] = None,
|
||||
e_weight: Optional[Union[float, List[float]]] = None,
|
||||
merge_op: str = "mean",
|
||||
group_name: str = "main",
|
||||
):
|
||||
r"""Add hyperedges to the hypergraph. If the ``group_name`` is not specified, the hyperedges will be added to the default ``main`` hyperedge group.
|
||||
|
||||
Args:
|
||||
``num_v`` (``int``): The number of vertices in the hypergraph.
|
||||
``e_list_v2e`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the vertices point to the hyperedges.
|
||||
``e_list_e2v`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the hyperedges point to the vertices.
|
||||
``w_list_v2e`` (``Union[List[float], List[List[float]]]``, optional): The weights are attached to the connections from vertices to hyperedges, which has the same shape
|
||||
as ``e_list_v2e``. If set to ``None``, the value ``1`` is used for all connections. Defaults to ``None``.
|
||||
``w_list_e2v`` (``Union[List[float], List[List[float]]]``, optional): The weights are attached to the connections from the hyperedges to the vertices, which has the
|
||||
same shape to ``e_list_e2v``. If set to ``None``, the value ``1`` is used for all connections. Defaults to ``None``.
|
||||
``e_weight`` (``Union[float, List[float]]``, optional): A list of weights for hyperedges. If set to ``None``, the value ``1`` is used for all hyperedges. Defaults to ``None``.
|
||||
``merge_op`` (``str``): The merge operation for the conflicting hyperedges. The possible values are ``mean``, ``sum``, ``max``, and ``min``. Defaults to ``mean``.
|
||||
``group_name`` (``str``, optional): The target hyperedge group to add these hyperedges. Defaults to the ``main`` hyperedge group.
|
||||
"""
|
||||
e_list_v2e, w_list_v2e = self._format_e_list_and_w_on_them(
|
||||
e_list_v2e, w_list_v2e
|
||||
)
|
||||
e_list_e2v, w_list_e2v = self._format_e_list_and_w_on_them(
|
||||
e_list_e2v, w_list_e2v
|
||||
)
|
||||
if e_weight is None:
|
||||
e_weight = [1.0] * len(e_list_v2e)
|
||||
assert len(e_list_v2e) == len(
|
||||
e_weight
|
||||
), "The number of hyperedges and the number of weights are not equal."
|
||||
assert len(e_list_v2e) == len(
|
||||
e_list_e2v
|
||||
), "Hyperedges of 'v2e' and 'e2v' must have the same size."
|
||||
for _idx in range(len(e_list_v2e)):
|
||||
self._add_hyperedge(
|
||||
self._hyperedge_code(e_list_v2e[_idx], e_list_e2v[_idx]),
|
||||
{
|
||||
"w_v2e": w_list_v2e[_idx],
|
||||
"w_e2v": w_list_e2v[_idx],
|
||||
"w_e": e_weight[_idx],
|
||||
},
|
||||
merge_op,
|
||||
group_name,
|
||||
)
|
||||
self._clear_cache(group_name)
|
||||
|
||||
def _add_hyperedge(
|
||||
self,
|
||||
hyperedge_code: Tuple[List[int], List[int]],
|
||||
content: Dict[str, Any],
|
||||
merge_op: str,
|
||||
group_name: str,
|
||||
):
|
||||
r"""Add a hyperedge to the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``hyperedge_code`` (``Tuple[List[int], List[int]]``): The hyperedge code.
|
||||
``content`` (``Dict[str, Any]``): The content of the hyperedge.
|
||||
``merge_op`` (``str``): The merge operation for the conflicting hyperedges.
|
||||
``group_name`` (``str``): The target hyperedge group to add this hyperedge.
|
||||
"""
|
||||
if group_name not in self._raw_groups:
|
||||
self._raw_groups[group_name] = {}
|
||||
self._raw_groups[group_name][hyperedge_code] = content
|
||||
else:
|
||||
if hyperedge_code not in self._raw_groups[group_name]:
|
||||
self._raw_groups[group_name][hyperedge_code] = content
|
||||
else:
|
||||
self._raw_groups[group_name][hyperedge_code] = self._merge_hyperedges(
|
||||
self._raw_groups[group_name][hyperedge_code], content, merge_op
|
||||
)
|
||||
|
||||
def remove_hyperedges(
|
||||
self,
|
||||
e_list_v2e: Union[List[int], List[List[int]]],
|
||||
e_list_e2v: Union[List[int], List[List[int]]],
|
||||
group_name: Optional[str] = None,
|
||||
):
|
||||
r"""Remove the specified hyperedges from the hypergraph.
|
||||
|
||||
Args:
|
||||
``e_list_v2e`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the vertices point to the hyperedges.
|
||||
``e_list_e2v`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the hyperedges point to the vertices.
|
||||
``group_name`` (``str``, optional): Remove these hyperedges from the specified hyperedge group. If not specified, the function will
|
||||
remove those hyperedges from all hyperedge groups. Defaults to the ``None``.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
assert len(e_list_v2e) == len(
|
||||
e_list_e2v
|
||||
), "Hyperedges of 'v2e' and 'e2v' must have the same size."
|
||||
e_list_v2e = self._format_e_list(e_list_v2e)
|
||||
e_list_e2v = self._format_e_list(e_list_e2v)
|
||||
if group_name is None:
|
||||
for _idx in range(len(e_list_v2e)):
|
||||
e_code = self._hyperedge_code(e_list_v2e[_idx], e_list_e2v[_idx])
|
||||
for name in self.group_names:
|
||||
self._raw_groups[name].pop(e_code, None)
|
||||
else:
|
||||
for _idx in range(len(e_list_v2e)):
|
||||
e_code = self._hyperedge_code(e_list_v2e[_idx], e_list_e2v[_idx])
|
||||
self._raw_groups[group_name].pop(e_code, None)
|
||||
self._clear_cache(group_name)
|
||||
|
||||
@abc.abstractmethod
|
||||
def drop_hyperedges(self, drop_rate: float, ord="uniform"):
|
||||
r"""Randomly drop hyperedges from the hypergraph. This function will return a new hypergraph with non-dropped hyperedges.
|
||||
|
||||
Args:
|
||||
``drop_rate`` (``float``): The drop rate of hyperedges.
|
||||
``ord`` (``str``): The order of dropping edges. Currently, only ``'uniform'`` is supported. Defaults to ``uniform``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def drop_hyperedges_of_group(
|
||||
self, group_name: str, drop_rate: float, ord="uniform"
|
||||
):
|
||||
r"""Randomly drop hyperedges from the specified hyperedge group. This function will return a new hypergraph with non-dropped hyperedges.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the hyperedge group.
|
||||
``drop_rate`` (``float``): The drop rate of hyperedges.
|
||||
``ord`` (``str``): The order of dropping edges. Currently, only ``'uniform'`` is supported. Defaults to ``uniform``.
|
||||
"""
|
||||
|
||||
# properties for the hypergraph
|
||||
@property
|
||||
def v(self) -> List[int]:
|
||||
r"""Return the list of vertices."""
|
||||
if self.cache.get("v") is None:
|
||||
self.cache["v"] = list(range(self.num_v))
|
||||
return self.cache["v"]
|
||||
|
||||
@property
|
||||
def v_weight(self) -> List[float]:
|
||||
r"""Return the vertex weights of the hypergraph."""
|
||||
if self._v_weight is None:
|
||||
self._v_weight = [1.0] * self.num_v
|
||||
return self._v_weight
|
||||
|
||||
@v_weight.setter
|
||||
def v_weight(self, v_weight: List[float]):
|
||||
r"""Set the vertex weights of the hypergraph."""
|
||||
assert (
|
||||
len(v_weight) == self.num_v
|
||||
), "The length of vertex weights must be equal to the number of vertices."
|
||||
self._v_weight = v_weight
|
||||
self._clear_cache()
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def e(self) -> Tuple[List[List[int]], List[float]]:
|
||||
r"""Return all hyperedges and weights in the hypergraph."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e_of_group(self, group_name: str) -> Tuple[List[List[int]], List[float]]:
|
||||
r"""Return all hyperedges and weights in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
|
||||
@property
|
||||
def v_property(self):
|
||||
return self._v_property
|
||||
|
||||
@property
|
||||
def e_property(self):
|
||||
group_e_property = {}
|
||||
for group in self._raw_groups:
|
||||
group_e_property[group] = list(self._raw_groups[group].values())
|
||||
return group_e_property
|
||||
|
||||
@property
|
||||
def num_v(self) -> int:
|
||||
r"""Return the number of vertices in the hypergraph."""
|
||||
return self._num_v
|
||||
|
||||
@property
|
||||
def num_e(self) -> int:
|
||||
r"""Return the number of hyperedges in the hypergraph."""
|
||||
_num_e = 0
|
||||
for name in self.group_names:
|
||||
_num_e += len(self._raw_groups[name])
|
||||
return _num_e
|
||||
|
||||
def num_e_of_group(self, group_name: str) -> int:
|
||||
r"""Return the number of hyperedges in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
return len(self._raw_groups[group_name])
|
||||
|
||||
@property
|
||||
def num_groups(self) -> int:
|
||||
r"""Return the number of hyperedge groups in the hypergraph."""
|
||||
return len(self._raw_groups)
|
||||
|
||||
@property
|
||||
def group_names(self) -> List[str]:
|
||||
r"""Return the names of hyperedge groups in the hypergraph."""
|
||||
return list(self._raw_groups.keys())
|
||||
|
||||
# properties for deep learning
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def vars_for_DL(self) -> List[str]:
|
||||
r"""Return a name list of available variables for deep learning in this type of hypergraph.
|
||||
"""
|
||||
|
||||
@property
|
||||
def W_v(self) -> torch.Tensor:
|
||||
r"""Return the vertex weight matrix of the hypergraph."""
|
||||
if self.cache["W_v"] is None:
|
||||
self.cache["W_v"] = torch.tensor(
|
||||
self.v_weight, dtype=torch.float, device=self.device
|
||||
).view(-1, 1)
|
||||
return self.cache["W_v"]
|
||||
|
||||
@property
|
||||
def W_e(self) -> torch.Tensor:
|
||||
r"""Return the hyperedge weight matrix of the hypergraph."""
|
||||
if self.cache["W_e"] is None:
|
||||
_tmp = [self.W_e_of_group(name) for name in self.group_names]
|
||||
self.cache["W_e"] = torch.cat(_tmp, dim=0)
|
||||
return self.cache["W_e"]
|
||||
|
||||
def W_e_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the hyperedge weight matrix of the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name]["W_e"] is None:
|
||||
self.group_cache[group_name]["W_e"] = self._fetch_W_of_group(group_name)
|
||||
return self.group_cache[group_name]["W_e"]
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def H(self) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix."""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def H_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
|
||||
@property
|
||||
def H_v2e(self) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix with ``sparse matrix`` format."""
|
||||
if self.cache.get("H_v2e") is None:
|
||||
_tmp = [self.H_v2e_of_group(name) for name in self.group_names]
|
||||
self.cache["H_v2e"] = torch.cat(_tmp, dim=1)
|
||||
return self.cache["H_v2e"]
|
||||
|
||||
def H_v2e_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix with ``sparse matrix`` format in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name].get("H_v2e") is None:
|
||||
self.group_cache[group_name]["H_v2e"] = self._fetch_H_of_group(
|
||||
"v2e", group_name
|
||||
)
|
||||
return self.group_cache[group_name]["H_v2e"]
|
||||
|
||||
@property
|
||||
def H_e2v(self) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix with ``sparse matrix`` format."""
|
||||
if self.cache.get("H_e2v") is None:
|
||||
_tmp = [self.H_e2v_of_group(name) for name in self.group_names]
|
||||
self.cache["H_e2v"] = torch.cat(_tmp, dim=1)
|
||||
return self.cache["H_e2v"]
|
||||
|
||||
def H_e2v_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the hypergraph incidence matrix with ``sparse matrix`` format in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name].get("H_e2v") is None:
|
||||
self.group_cache[group_name]["H_e2v"] = self._fetch_H_of_group(
|
||||
"e2v", group_name
|
||||
)
|
||||
return self.group_cache[group_name]["H_e2v"]
|
||||
|
||||
@property
|
||||
def R_v2e(self) -> torch.Tensor:
|
||||
r"""Return the weight matrix of connections (vertices point to hyperedges) with ``sparse matrix`` format.
|
||||
"""
|
||||
if self.cache.get("R_v2e") is None:
|
||||
_tmp = [self.R_v2e_of_group(name) for name in self.group_names]
|
||||
self.cache["R_v2e"] = torch.cat(_tmp, dim=1)
|
||||
return self.cache["R_v2e"]
|
||||
|
||||
def R_v2e_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the weight matrix of connections (vertices point to hyperedges) with ``sparse matrix`` format in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name].get("R_v2e") is None:
|
||||
self.group_cache[group_name]["R_v2e"] = self._fetch_R_of_group(
|
||||
"v2e", group_name
|
||||
)
|
||||
return self.group_cache[group_name]["R_v2e"]
|
||||
|
||||
@property
|
||||
def R_e2v(self) -> torch.Tensor:
|
||||
r"""Return the weight matrix of connections (hyperedges point to vertices) with ``sparse matrix`` format.
|
||||
"""
|
||||
if self.cache.get("R_e2v") is None:
|
||||
_tmp = [self.R_e2v_of_group(name) for name in self.group_names]
|
||||
self.cache["R_e2v"] = torch.cat(_tmp, dim=1)
|
||||
return self.cache["R_e2v"]
|
||||
|
||||
def R_e2v_of_group(self, group_name: str) -> torch.Tensor:
|
||||
r"""Return the weight matrix of connections (hyperedges point to vertices) with ``sparse matrix`` format in the specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The name of the specified hyperedge group.
|
||||
"""
|
||||
assert (
|
||||
group_name in self.group_names
|
||||
), f"The specified {group_name} is not in existing hyperedge groups."
|
||||
if self.group_cache[group_name].get("R_e2v") is None:
|
||||
self.group_cache[group_name]["R_e2v"] = self._fetch_R_of_group(
|
||||
"e2v", group_name
|
||||
)
|
||||
return self.group_cache[group_name]["R_e2v"]
|
||||
|
||||
# spectral-based smoothing
|
||||
def smoothing(self, X: torch.Tensor, L: torch.Tensor, lamb: float) -> torch.Tensor:
|
||||
r"""Spectral-based smoothing.
|
||||
|
||||
.. math::
|
||||
X_{smoothed} = X + \lambda \mathcal{L} X
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): The vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``L`` (``torch.Tensor``): The Laplacian matrix with ``torch.sparse_coo_tensor`` format. Size :math:`(|\mathcal{V}|, |\mathcal{V}|)`.
|
||||
``lamb`` (``float``): :math:`\lambda`, the strength of smoothing.
|
||||
"""
|
||||
return X + lamb * torch.sparse.mm(L, X)
|
||||
|
||||
# message passing functions
|
||||
@abc.abstractmethod
|
||||
def v2e_aggregation(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
drop_rate: float = 0.0,
|
||||
):
|
||||
r"""Message aggretation step of ``vertices to hyperedges``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e_aggregation_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
drop_rate: float = 0.0,
|
||||
):
|
||||
r"""Message aggregation step of ``vertices to hyperedges`` in specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e_update(self, X: torch.Tensor, e_weight: Optional[torch.Tensor] = None):
|
||||
r"""Message update step of ``vertices to hyperedges``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e_update_of_group(
|
||||
self, group_name: str, X: torch.Tensor, e_weight: Optional[torch.Tensor] = None
|
||||
):
|
||||
r"""Message update step of ``vertices to hyperedges`` in specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
e_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``vertices to hyperedges``. The combination of ``v2e_aggregation`` and ``v2e_update``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2e_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
e_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``vertices to hyperedges`` in specified hyperedge group. The combination of ``e2v_aggregation_of_group`` and ``e2v_update_of_group``.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_aggregation(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message aggregation step of ``hyperedges to vertices``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_aggregation_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message aggregation step of ``hyperedges to vertices`` in specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_update(self, X: torch.Tensor, v_weight: Optional[torch.Tensor] = None):
|
||||
r"""Message update step of ``hyperedges to vertices``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_update_of_group(
|
||||
self, group_name: str, X: torch.Tensor, v_weight: Optional[torch.Tensor] = None
|
||||
):
|
||||
r"""Message update step of ``hyperedges to vertices`` in specified hyperedge group.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``hyperedges to vertices``. The combination of ``e2v_aggregation`` and ``e2v_update``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def e2v_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``hyperedges to vertices`` in specified hyperedge group. The combination of ``e2v_aggregation_of_group`` and ``e2v_update_of_group``.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Hyperedge feature matrix. Size :math:`(|\mathcal{E}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2v(
|
||||
self,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_aggr: Optional[str] = None,
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
e_weight: Optional[torch.Tensor] = None,
|
||||
e2v_aggr: Optional[str] = None,
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``vertices to vertices``. The combination of ``v2e`` and ``e2v``.
|
||||
|
||||
Args:
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, this ``aggr`` will be used to both ``v2e`` and ``e2v``.
|
||||
``v2e_aggr`` (``str``, optional): The aggregation method for hyperedges to vertices. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, it will override the ``aggr`` in ``e2v``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e2v_aggr`` (``str``, optional): The aggregation method for vertices to hyperedges. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, it will override the ``aggr`` in ``v2e``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def v2v_of_group(
|
||||
self,
|
||||
group_name: str,
|
||||
X: torch.Tensor,
|
||||
aggr: str = "mean",
|
||||
v2e_aggr: Optional[str] = None,
|
||||
v2e_weight: Optional[torch.Tensor] = None,
|
||||
e_weight: Optional[torch.Tensor] = None,
|
||||
e2v_aggr: Optional[str] = None,
|
||||
e2v_weight: Optional[torch.Tensor] = None,
|
||||
v_weight: Optional[torch.Tensor] = None,
|
||||
):
|
||||
r"""Message passing of ``vertices to vertices`` in specified hyperedge group. The combination of ``v2e_of_group`` and ``e2v_of_group``.
|
||||
|
||||
Args:
|
||||
``group_name`` (``str``): The specified hyperedge group.
|
||||
``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`.
|
||||
``aggr`` (``str``): The aggregation method. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, this ``aggr`` will be used to both ``v2e_of_group`` and ``e2v_of_group``.
|
||||
``v2e_aggr`` (``str``, optional): The aggregation method for hyperedges to vertices. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, it will override the ``aggr`` in ``e2v_of_group``.
|
||||
``v2e_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (vertices point to hyepredges). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e_weight`` (``torch.Tensor``, optional): The hyperedge weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``e2v_aggr`` (``str``, optional): The aggregation method for vertices to hyperedges. Can be ``'mean'``, ``'sum'`` and ``'softmax_then_sum'``. If specified, it will override the ``aggr`` in ``v2e_of_group``.
|
||||
``e2v_weight`` (``torch.Tensor``, optional): The weight vector attached to connections (hyperedges point to vertices). If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
``v_weight`` (``torch.Tensor``, optional): The vertex weight vector. If not specified, the function will use the weights specified in hypergraph construction. Defaults to ``None``.
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,420 @@
|
||||
from copy import deepcopy
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
import easygraph as eg
|
||||
import easygraph.convert as convert
|
||||
|
||||
from easygraph.classes.directed_graph import DiGraph
|
||||
from easygraph.classes.multigraph import MultiGraph
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["MultiDiGraph"]
|
||||
|
||||
|
||||
class MultiDiGraph(MultiGraph, DiGraph):
|
||||
edge_key_dict_factory = dict
|
||||
|
||||
def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr):
|
||||
"""Initialize a graph with edges, name, or graph attributes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
incoming_graph_data : input graph
|
||||
Data to initialize graph. If incoming_graph_data=None (default)
|
||||
an empty graph is created. The data can be an edge list, or any
|
||||
EasyGraph graph object. If the corresponding optional Python
|
||||
packages are installed the data can also be a NumPy matrix
|
||||
or 2d ndarray, a SciPy sparse matrix, or a PyGraphviz graph.
|
||||
|
||||
multigraph_input : bool or None (default None)
|
||||
Note: Only used when `incoming_graph_data` is a dict.
|
||||
If True, `incoming_graph_data` is assumed to be a
|
||||
dict-of-dict-of-dict-of-dict structure keyed by
|
||||
node to neighbor to edge keys to edge data for multi-edges.
|
||||
A EasyGraphError is raised if this is not the case.
|
||||
If False, :func:`to_easygraph_graph` is used to try to determine
|
||||
the dict's graph data structure as either a dict-of-dict-of-dict
|
||||
keyed by node to neighbor to edge data, or a dict-of-iterable
|
||||
keyed by node to neighbors.
|
||||
If None, the treatment for True is tried, but if it fails,
|
||||
the treatment for False is tried.
|
||||
|
||||
attr : keyword arguments, optional (default= no attributes)
|
||||
Attributes to add to graph as key=value pairs.
|
||||
|
||||
See Also
|
||||
--------
|
||||
convert
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G = eg.Graph(name="my graph")
|
||||
>>> e = [(1, 2), (2, 3), (3, 4)] # list of edges
|
||||
>>> G = eg.Graph(e)
|
||||
|
||||
Arbitrary graph attribute pairs (key=value) may be assigned
|
||||
|
||||
>>> G = eg.Graph(e, day="Friday")
|
||||
>>> G.graph
|
||||
{'day': 'Friday'}
|
||||
|
||||
"""
|
||||
self.edge_key_dict_factory = self.edge_key_dict_factory
|
||||
# multigraph_input can be None/True/False. So check "is not False"
|
||||
if isinstance(incoming_graph_data, dict) and multigraph_input is not False:
|
||||
DiGraph.__init__(self)
|
||||
try:
|
||||
convert.from_dict_of_dicts(
|
||||
incoming_graph_data, create_using=self, multigraph_input=True
|
||||
)
|
||||
self.graph.update(attr)
|
||||
except Exception as err:
|
||||
if multigraph_input is True:
|
||||
raise EasyGraphError(
|
||||
f"converting multigraph_input raised:\n{type(err)}: {err}"
|
||||
)
|
||||
DiGraph.__init__(self, incoming_graph_data, **attr)
|
||||
else:
|
||||
DiGraph.__init__(self, incoming_graph_data, **attr)
|
||||
|
||||
def add_edge(self, u_for_edge, v_for_edge, key=None, **attr):
|
||||
"""Add an edge between u and v.
|
||||
|
||||
The nodes u and v will be automatically added if they are
|
||||
not already in the graph.
|
||||
|
||||
Edge attributes can be specified with keywords or by directly
|
||||
accessing the edge's attribute dictionary. See examples below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u_for_edge, v_for_edge : nodes
|
||||
Nodes can be, for example, strings or numbers.
|
||||
Nodes must be hashable (and not None) Python objects.
|
||||
key : hashable identifier, optional (default=lowest unused integer)
|
||||
Used to distinguish multiedges between a pair of nodes.
|
||||
attr : keyword arguments, optional
|
||||
Edge data (or labels or objects) can be assigned using
|
||||
keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The edge key assigned to the edge.
|
||||
|
||||
See Also
|
||||
--------
|
||||
add_edges_from : add a collection of edges
|
||||
|
||||
Notes
|
||||
-----
|
||||
To replace/update edge data, use the optional key argument
|
||||
to identify a unique edge. Otherwise a new edge will be created.
|
||||
|
||||
EasyGraph algorithms designed for weighted graphs cannot use
|
||||
multigraphs directly because it is not clear how to handle
|
||||
multiedge weights. Convert to Graph using edge attribute
|
||||
'weight' to enable weighted graph algorithms.
|
||||
|
||||
Default keys are generated using the method `new_edge_key()`.
|
||||
This method can be overridden by subclassing the base class and
|
||||
providing a custom `new_edge_key()` method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following all add the edge e=(1, 2) to graph G:
|
||||
|
||||
>>> G = eg.MultiDiGraph()
|
||||
>>> e = (1, 2)
|
||||
>>> key = G.add_edge(1, 2) # explicit two-node form
|
||||
>>> G.add_edge(*e) # single edge as tuple of two nodes
|
||||
1
|
||||
>>> G.add_edges_from([(1, 2)]) # add edges from iterable container
|
||||
[2]
|
||||
|
||||
Associate data to edges using keywords:
|
||||
|
||||
>>> key = G.add_edge(1, 2, weight=3)
|
||||
>>> key = G.add_edge(1, 2, key=0, weight=4) # update data for key=0
|
||||
>>> key = G.add_edge(1, 3, weight=7, capacity=15, length=342.7)
|
||||
|
||||
For non-string attribute keys, use subscript notation.
|
||||
|
||||
>>> ekey = G.add_edge(1, 2)
|
||||
>>> G[1][2][0].update({0: 5})
|
||||
>>> G.edges[1, 2, 0].update({0: 5})
|
||||
|
||||
>>>
|
||||
>>>
|
||||
"""
|
||||
u, v = u_for_edge, v_for_edge
|
||||
if "attr" in attr:
|
||||
temp = attr.get("attr")
|
||||
attr = temp if temp != None else {}
|
||||
# add nodes
|
||||
if u not in self._adj:
|
||||
if u is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
self._adj[u] = self.adjlist_inner_dict_factory()
|
||||
self._pred[u] = self.adjlist_inner_dict_factory()
|
||||
self._node[u] = self.node_attr_dict_factory()
|
||||
if v not in self._adj:
|
||||
if v is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
self._adj[v] = self.adjlist_inner_dict_factory()
|
||||
self._pred[v] = self.adjlist_inner_dict_factory()
|
||||
self._node[v] = self.node_attr_dict_factory()
|
||||
if key is None:
|
||||
key = self.new_edge_key(u, v)
|
||||
if v in self._adj[u]:
|
||||
keydict = self._adj[u][v]
|
||||
datadict = keydict.get(key, self.edge_key_dict_factory())
|
||||
datadict.update(attr)
|
||||
keydict[key] = datadict
|
||||
else:
|
||||
# selfloops work this way without special treatment
|
||||
datadict = self.edge_attr_dict_factory()
|
||||
datadict.update(attr)
|
||||
keydict = self.edge_key_dict_factory()
|
||||
keydict[key] = datadict
|
||||
self._adj[u][v] = keydict
|
||||
self._pred[v][u] = keydict
|
||||
return key
|
||||
|
||||
def remove_edge(self, u, v, key=None):
|
||||
"""Remove an edge between u and v.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
Remove an edge between nodes u and v.
|
||||
key : hashable identifier, optional (default=None)
|
||||
Used to distinguish multiple edges between a pair of nodes.
|
||||
If None remove a single (arbitrary) edge between u and v.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If there is not an edge between u and v, or
|
||||
if there is no edge with the specified key.
|
||||
|
||||
See Also
|
||||
--------
|
||||
remove_edges_from : remove a collection of edges
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.MultiDiGraph()
|
||||
>>> G.add_edges_from([(1, 2), (1, 2), (1, 2)]) # key_list returned
|
||||
[0, 1, 2]
|
||||
>>> G.remove_edge(1, 2) # remove a single (arbitrary) edge
|
||||
|
||||
For edges with keys
|
||||
|
||||
>>> G = eg.MultiDiGraph()
|
||||
>>> G.add_edge(1, 2, key="first")
|
||||
'first'
|
||||
>>> G.add_edge(1, 2, key="second")
|
||||
'second'
|
||||
>>> G.remove_edge(1, 2, key="second")
|
||||
|
||||
"""
|
||||
try:
|
||||
d = self._adj[u][v]
|
||||
except KeyError as err:
|
||||
raise EasyGraphError(f"The edge {u}-{v} is not in the graph.") from err
|
||||
# remove the edge with specified data
|
||||
if key is None:
|
||||
d.popitem()
|
||||
else:
|
||||
try:
|
||||
del d[key]
|
||||
except KeyError as err:
|
||||
msg = f"The edge {u}-{v} with key {key} is not in the graph."
|
||||
raise EasyGraphError(msg) from err
|
||||
if len(d) == 0:
|
||||
# remove the key entries if last edge
|
||||
del self._adj[u][v]
|
||||
del self._pred[v][u]
|
||||
|
||||
@property
|
||||
def edges(self):
|
||||
edges = list()
|
||||
for n, nbrs in self._adj.items():
|
||||
for nbr, kd in nbrs.items():
|
||||
for k, dd in kd.items():
|
||||
edges.append((n, nbr, k, dd))
|
||||
return edges
|
||||
|
||||
out_edges = edges
|
||||
|
||||
@property
|
||||
def in_edges(self):
|
||||
edges = list()
|
||||
for n, nbrs in self._adj.items():
|
||||
for nbr, kd in nbrs.items():
|
||||
for k, dd in kd.items():
|
||||
edges.append((nbr, n, k))
|
||||
return edges
|
||||
|
||||
@property
|
||||
def degree(self, weight="weight"):
|
||||
degree = dict()
|
||||
if weight is None:
|
||||
for n in self._node:
|
||||
succs = self._adj[n]
|
||||
preds = self._pred[n]
|
||||
deg = sum(len(keys) for keys in succs.values()) + sum(
|
||||
len(keys) for keys in preds.values()
|
||||
)
|
||||
degree[n] = deg
|
||||
else:
|
||||
for n in self._node:
|
||||
succs = self._adj[n]
|
||||
preds = self._pred[n]
|
||||
deg = sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in succs.values()
|
||||
for d in key_dict.values()
|
||||
) + sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in preds.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
degree[n] = deg
|
||||
|
||||
@property
|
||||
def in_degree(self, weight="weight"):
|
||||
degree = dict()
|
||||
if weight is None:
|
||||
for n in self._node:
|
||||
preds = self._pred[n]
|
||||
deg = sum(len(keys) for keys in preds.values())
|
||||
degree[n] = deg
|
||||
else:
|
||||
for n in self._node:
|
||||
preds = self._pred[n]
|
||||
deg = sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in preds.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
degree[n] = deg
|
||||
|
||||
@property
|
||||
def out_degree(self, weight="weight"):
|
||||
degree = dict()
|
||||
if weight is None:
|
||||
for n in self._node:
|
||||
succs = self._adj[n]
|
||||
deg = sum(len(keys) for keys in succs.values())
|
||||
degree[n] = deg
|
||||
else:
|
||||
for n in self._node:
|
||||
succs = self._adj[n]
|
||||
deg = sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in succs.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
degree[n] = deg
|
||||
|
||||
def is_multigraph(self):
|
||||
"""Returns True if graph is a multigraph, False otherwise."""
|
||||
return True
|
||||
|
||||
def is_directed(self):
|
||||
"""Returns True if graph is directed, False otherwise."""
|
||||
return True
|
||||
|
||||
def to_undirected(self, reciprocal=False):
|
||||
"""Returns an undirected representation of the multidigraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reciprocal : bool (optional)
|
||||
If True only keep edges that appear in both directions
|
||||
in the original digraph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : MultiGraph
|
||||
An undirected graph with the same name and nodes and
|
||||
with edge (u, v, data) if either (u, v, data) or (v, u, data)
|
||||
is in the digraph. If both edges exist in digraph and
|
||||
their edge data is different, only one edge is created
|
||||
with an arbitrary choice of which edge data to use.
|
||||
You must check and correct for this manually if desired.
|
||||
|
||||
See Also
|
||||
--------
|
||||
MultiGraph, add_edge, add_edges_from
|
||||
|
||||
Notes
|
||||
-----
|
||||
This returns a "deepcopy" of the edge, node, and
|
||||
graph attributes which attempts to completely copy
|
||||
all of the data and references.
|
||||
|
||||
This is in contrast to the similar D=MultiDiGraph(G) which
|
||||
returns a shallow copy of the data.
|
||||
|
||||
See the Python copy module for more information on shallow
|
||||
and deep copies, https://docs.python.org/3/library/copy.html.
|
||||
|
||||
Warning: If you have subclassed MultiDiGraph to use dict-like
|
||||
objects in the data structure, those changes do not transfer
|
||||
to the MultiGraph created by this method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(2) # or MultiGraph, etc
|
||||
>>> H = G.to_directed()
|
||||
>>> list(H.edges)
|
||||
[(0, 1), (1, 0)]
|
||||
>>> G2 = H.to_undirected()
|
||||
>>> list(G2.edges)
|
||||
[(0, 1)]
|
||||
"""
|
||||
G = eg.MultiGraph()
|
||||
G.graph.update(deepcopy(self.graph))
|
||||
G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
|
||||
if reciprocal is True:
|
||||
G.add_edges_from(
|
||||
(u, v, key, deepcopy(data))
|
||||
for u, nbrs in self._adj.items()
|
||||
for v, keydict in nbrs.items()
|
||||
for key, data in keydict.items()
|
||||
if v in self._pred[u] and key in self._pred[u][v]
|
||||
)
|
||||
else:
|
||||
G.add_edges_from(
|
||||
(u, v, key, deepcopy(data))
|
||||
for u, nbrs in self._adj.items()
|
||||
for v, keydict in nbrs.items()
|
||||
for key, data in keydict.items()
|
||||
)
|
||||
return G
|
||||
|
||||
def reverse(self, copy=True):
|
||||
"""Returns the reverse of the graph.
|
||||
|
||||
The reverse is a graph with the same nodes and edges
|
||||
but with the directions of the edges reversed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
copy : bool optional (default=True)
|
||||
If True, return a new DiGraph holding the reversed edges.
|
||||
If False, the reverse graph is created using a view of
|
||||
the original graph.
|
||||
"""
|
||||
if copy:
|
||||
H = self.__class__()
|
||||
H.graph.update(deepcopy(self.graph))
|
||||
H.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
|
||||
H.add_edges_from((v, u, k, deepcopy(d)) for u, v, k, d in self.edges)
|
||||
return H
|
||||
return eg.graphviews.reverse_view(self)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
from easygraph.utils import only_implemented_for_Directed_graph
|
||||
|
||||
|
||||
__all__ = ["reverse_view"]
|
||||
|
||||
|
||||
@only_implemented_for_Directed_graph
|
||||
def reverse_view(G):
|
||||
newG = G.__class__()
|
||||
newG._graph = G
|
||||
newG.graph = G.graph
|
||||
newG._node = G._node
|
||||
newG._succ, newG._pred = G._pred, G._succ
|
||||
newG._adj = newG._succ
|
||||
return newG
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,729 @@
|
||||
"""Base class for MultiGraph."""
|
||||
from copy import deepcopy
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
import easygraph as eg
|
||||
import easygraph.convert as convert
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["MultiGraph"]
|
||||
|
||||
|
||||
class MultiGraph(Graph):
|
||||
edge_key_dict_factory = dict
|
||||
|
||||
def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr):
|
||||
"""Initialize a graph with edges, name, or graph attributes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
incoming_graph_data : input graph
|
||||
Data to initialize graph. If incoming_graph_data=None (default)
|
||||
an empty graph is created. The data can be an edge list, or any
|
||||
EasyGraph graph object. If the corresponding optional Python
|
||||
packages are installed the data can also be a NumPy matrix
|
||||
or 2d ndarray, a SciPy sparse matrix, or a PyGraphviz graph.
|
||||
|
||||
multigraph_input : bool or None (default None)
|
||||
Note: Only used when `incoming_graph_data` is a dict.
|
||||
If True, `incoming_graph_data` is assumed to be a
|
||||
dict-of-dict-of-dict-of-dict structure keyed by
|
||||
node to neighbor to edge keys to edge data for multi-edges.
|
||||
A EasyGraphError is raised if this is not the case.
|
||||
If False, :func:`to_easygraph_graph` is used to try to determine
|
||||
the dict's graph data structure as either a dict-of-dict-of-dict
|
||||
keyed by node to neighbor to edge data, or a dict-of-iterable
|
||||
keyed by node to neighbors.
|
||||
If None, the treatment for True is tried, but if it fails,
|
||||
the treatment for False is tried.
|
||||
|
||||
attr : keyword arguments, optional (default= no attributes)
|
||||
Attributes to add to graph as key=value pairs.
|
||||
|
||||
See Also
|
||||
--------
|
||||
convert
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G = eg.Graph(name="my graph")
|
||||
>>> e = [(1, 2), (2, 3), (3, 4)] # list of edges
|
||||
>>> G = eg.Graph(e)
|
||||
|
||||
Arbitrary graph attribute pairs (key=value) may be assigned
|
||||
|
||||
>>> G = eg.Graph(e, day="Friday")
|
||||
>>> G.graph
|
||||
{'day': 'Friday'}
|
||||
|
||||
"""
|
||||
self.edge_key_dict_factory = self.edge_key_dict_factory
|
||||
if isinstance(incoming_graph_data, dict) and multigraph_input is not False:
|
||||
Graph.__init__(self)
|
||||
try:
|
||||
convert.from_dict_of_dicts(
|
||||
incoming_graph_data, create_using=self, multigraph_input=True
|
||||
)
|
||||
self.graph.update(attr)
|
||||
except Exception as err:
|
||||
if multigraph_input is True:
|
||||
raise eg.EasyGraphError(
|
||||
f"converting multigraph_input raised:\n{type(err)}: {err}"
|
||||
)
|
||||
Graph.__init__(self, incoming_graph_data, **attr)
|
||||
else:
|
||||
Graph.__init__(self, incoming_graph_data, **attr)
|
||||
|
||||
def new_edge_key(self, u, v):
|
||||
"""Returns an unused key for edges between nodes `u` and `v`.
|
||||
|
||||
The nodes `u` and `v` do not need to be already in the graph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
In the standard MultiGraph class the new key is the number of existing
|
||||
edges between `u` and `v` (increased if necessary to ensure unused).
|
||||
The first edge will have key 0, then 1, etc. If an edge is removed
|
||||
further new_edge_keys may not be in this order.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
|
||||
Returns
|
||||
-------
|
||||
key : int
|
||||
"""
|
||||
try:
|
||||
keydict = self._adj[u][v]
|
||||
except KeyError:
|
||||
return 0
|
||||
key = len(keydict)
|
||||
while key in keydict:
|
||||
key += 1
|
||||
return key
|
||||
|
||||
def add_edge(self, u_for_edge, v_for_edge, key=None, **attr):
|
||||
"""Add an edge between u and v.
|
||||
|
||||
The nodes u and v will be automatically added if they are
|
||||
not already in the graph.
|
||||
|
||||
Edge attributes can be specified with keywords or by directly
|
||||
accessing the edge's attribute dictionary. See examples below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u_for_edge, v_for_edge : nodes
|
||||
Nodes can be, for example, strings or numbers.
|
||||
Nodes must be hashable (and not None) Python objects.
|
||||
key : hashable identifier, optional (default=lowest unused integer)
|
||||
Used to distinguish multiedges between a pair of nodes.
|
||||
attr : keyword arguments, optional
|
||||
Edge data (or labels or objects) can be assigned using
|
||||
keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The edge key assigned to the edge.
|
||||
|
||||
See Also
|
||||
--------
|
||||
add_edges_from : add a collection of edges
|
||||
|
||||
Notes
|
||||
-----
|
||||
To replace/update edge data, use the optional key argument
|
||||
to identify a unique edge. Otherwise a new edge will be created.
|
||||
|
||||
EasyGraph algorithms designed for weighted graphs cannot use
|
||||
multigraphs directly because it is not clear how to handle
|
||||
multiedge weights. Convert to Graph using edge attribute
|
||||
'weight' to enable weighted graph algorithms.
|
||||
|
||||
Default keys are generated using the method `new_edge_key()`.
|
||||
This method can be overridden by subclassing the base class and
|
||||
providing a custom `new_edge_key()` method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following all add the edge e=(1, 2) to graph G:
|
||||
|
||||
>>> G = eg.MultiGraph()
|
||||
>>> e = (1, 2)
|
||||
>>> ekey = G.add_edge(1, 2) # explicit two-node form
|
||||
>>> G.add_edge(*e) # single edge as tuple of two nodes
|
||||
1
|
||||
>>> G.add_edges_from([(1, 2)]) # add edges from iterable container
|
||||
[2]
|
||||
|
||||
Associate data to edges using keywords:
|
||||
|
||||
>>> ekey = G.add_edge(1, 2, weight=3)
|
||||
>>> ekey = G.add_edge(1, 2, key=0, weight=4) # update data for key=0
|
||||
>>> ekey = G.add_edge(1, 3, weight=7, capacity=15, length=342.7)
|
||||
|
||||
For non-string attribute keys, use subscript notation.
|
||||
|
||||
>>> ekey = G.add_edge(1, 2)
|
||||
>>> G[1][2][0].update({0: 5})
|
||||
>>> G.edges[1, 2, 0].update({0: 5})
|
||||
"""
|
||||
u, v = u_for_edge, v_for_edge
|
||||
# add nodes
|
||||
if u not in self._adj:
|
||||
if u is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
self._adj[u] = self.adjlist_inner_dict_factory()
|
||||
self._node[u] = self.node_attr_dict_factory()
|
||||
if v not in self._adj:
|
||||
if v is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
self._adj[v] = self.adjlist_inner_dict_factory()
|
||||
self._node[v] = self.node_attr_dict_factory()
|
||||
if key is None:
|
||||
key = self.new_edge_key(u, v)
|
||||
if v in self._adj[u]:
|
||||
keydict = self._adj[u][v]
|
||||
datadict = keydict.get(key, self.edge_attr_dict_factory())
|
||||
datadict.update(attr)
|
||||
keydict[key] = datadict
|
||||
else:
|
||||
# selfloops work this way without special treatment
|
||||
datadict = self.edge_attr_dict_factory()
|
||||
datadict.update(attr)
|
||||
keydict = self.edge_key_dict_factory()
|
||||
keydict[key] = datadict
|
||||
self._adj[u][v] = keydict
|
||||
self._adj[v][u] = keydict
|
||||
return key
|
||||
|
||||
def add_edges_from(self, ebunch_to_add, **attr):
|
||||
"""Add all the edges in ebunch_to_add.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ebunch_to_add : container of edges
|
||||
Each edge given in the container will be added to the
|
||||
graph. The edges can be:
|
||||
|
||||
- 2-tuples (u, v) or
|
||||
- 3-tuples (u, v, d) for an edge data dict d, or
|
||||
- 3-tuples (u, v, k) for not iterable key k, or
|
||||
- 4-tuples (u, v, k, d) for an edge with data and key k
|
||||
|
||||
attr : keyword arguments, optional
|
||||
Edge data (or labels or objects) can be assigned using
|
||||
keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A list of edge keys assigned to the edges in `ebunch`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
add_edge : add a single edge
|
||||
add_weighted_edges_from : convenient way to add weighted edges
|
||||
|
||||
Notes
|
||||
-----
|
||||
Adding the same edge twice has no effect but any edge data
|
||||
will be updated when each duplicate edge is added.
|
||||
|
||||
Edge attributes specified in an ebunch take precedence over
|
||||
attributes specified via keyword arguments.
|
||||
|
||||
Default keys are generated using the method ``new_edge_key()``.
|
||||
This method can be overridden by subclassing the base class and
|
||||
providing a custom ``new_edge_key()`` method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G.add_edges_from([(0, 1), (1, 2)]) # using a list of edge tuples
|
||||
>>> e = zip(range(0, 3), range(1, 4))
|
||||
>>> G.add_edges_from(e) # Add the path graph 0-1-2-3
|
||||
|
||||
Associate data to edges
|
||||
|
||||
>>> G.add_edges_from([(1, 2), (2, 3)], weight=3)
|
||||
>>> G.add_edges_from([(3, 4), (1, 4)], label="WN2898")
|
||||
"""
|
||||
keylist = []
|
||||
for e in ebunch_to_add:
|
||||
ne = len(e)
|
||||
if ne == 4:
|
||||
u, v, key, dd = e
|
||||
elif ne == 3:
|
||||
u, v, dd = e
|
||||
key = None
|
||||
elif ne == 2:
|
||||
u, v = e
|
||||
dd = {}
|
||||
key = None
|
||||
else:
|
||||
msg = f"Edge tuple {e} must be a 2-tuple, 3-tuple or 4-tuple."
|
||||
raise EasyGraphError(msg)
|
||||
ddd = {}
|
||||
ddd.update(attr)
|
||||
try:
|
||||
ddd.update(dd)
|
||||
except (TypeError, ValueError):
|
||||
if ne != 3:
|
||||
raise
|
||||
key = dd # ne == 3 with 3rd value not dict, must be a key
|
||||
key = self.add_edge(u, v, key)
|
||||
self[u][v][key].update(ddd)
|
||||
keylist.append(key)
|
||||
return keylist
|
||||
|
||||
def remove_edge(self, u, v, key=None):
|
||||
"""Remove an edge between u and v.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
Remove an edge between nodes u and v.
|
||||
key : hashable identifier, optional (default=None)
|
||||
Used to distinguish multiple edges between a pair of nodes.
|
||||
If None remove a single (arbitrary) edge between u and v.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If there is not an edge between u and v, or
|
||||
if there is no edge with the specified key.
|
||||
|
||||
See Also
|
||||
--------
|
||||
remove_edges_from : remove a collection of edges
|
||||
|
||||
Examples
|
||||
--------
|
||||
For multiple edges
|
||||
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph, etc
|
||||
>>> G.add_edges_from([(1, 2), (1, 2), (1, 2)]) # key_list returned
|
||||
[0, 1, 2]
|
||||
>>> G.remove_edge(1, 2) # remove a single (arbitrary) edge
|
||||
|
||||
For edges with keys
|
||||
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph, etc
|
||||
>>> G.add_edge(1, 2, key="first")
|
||||
'first'
|
||||
>>> G.add_edge(1, 2, key="second")
|
||||
'second'
|
||||
>>> G.remove_edge(1, 2, key="second")
|
||||
|
||||
"""
|
||||
try:
|
||||
d = self._adj[u][v]
|
||||
except KeyError as err:
|
||||
raise EasyGraphError(f"The edge {u}-{v} is not in the graph.") from err
|
||||
# remove the edge with specified data
|
||||
if key is None:
|
||||
d.popitem()
|
||||
else:
|
||||
try:
|
||||
del d[key]
|
||||
except KeyError as err:
|
||||
msg = f"The edge {u}-{v} with key {key} is not in the graph."
|
||||
raise EasyGraphError(msg) from err
|
||||
if len(d) == 0:
|
||||
# remove the key entries if last edge
|
||||
del self._adj[u][v]
|
||||
if u != v: # check for selfloop
|
||||
del self._adj[v][u]
|
||||
|
||||
def remove_edges_from(self, ebunch):
|
||||
"""Remove all edges specified in ebunch.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ebunch: list or container of edge tuples
|
||||
Each edge given in the list or container will be removed
|
||||
from the graph. The edges can be:
|
||||
|
||||
- 2-tuples (u, v) All edges between u and v are removed.
|
||||
- 3-tuples (u, v, key) The edge identified by key is removed.
|
||||
- 4-tuples (u, v, key, data) where data is ignored.
|
||||
|
||||
See Also
|
||||
--------
|
||||
remove_edge : remove a single edge
|
||||
|
||||
Notes
|
||||
-----
|
||||
Will fail silently if an edge in ebunch is not in the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Removing multiple copies of edges
|
||||
|
||||
>>> G = eg.MultiGraph()
|
||||
>>> keys = G.add_edges_from([(1, 2), (1, 2), (1, 2)])
|
||||
>>> G.remove_edges_from([(1, 2), (1, 2)])
|
||||
>>> list(G.edges())
|
||||
[(1, 2)]
|
||||
>>> G.remove_edges_from([(1, 2), (1, 2)]) # silently ignore extra copy
|
||||
>>> list(G.edges) # now empty graph
|
||||
[]
|
||||
"""
|
||||
for e in ebunch:
|
||||
try:
|
||||
self.remove_edge(*e[:3])
|
||||
except EasyGraphError:
|
||||
pass
|
||||
|
||||
def has_edge(self, u, v, key=None):
|
||||
"""Returns True if the graph has an edge between nodes u and v.
|
||||
|
||||
This is the same as `v in G[u] or key in G[u][v]`
|
||||
without KeyError exceptions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
Nodes can be, for example, strings or numbers.
|
||||
|
||||
key : hashable identifier, optional (default=None)
|
||||
If specified return True only if the edge with
|
||||
key is found.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edge_ind : bool
|
||||
True if edge is in the graph, False otherwise.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Can be called either using two nodes u, v, an edge tuple (u, v),
|
||||
or an edge tuple (u, v, key).
|
||||
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph
|
||||
>>> G = eg.complete_graph(4, create_using=eg.MultiDiGraph)
|
||||
>>> G.has_edge(0, 1) # using two nodes
|
||||
True
|
||||
>>> e = (0, 1)
|
||||
>>> G.has_edge(*e) # e is a 2-tuple (u, v)
|
||||
True
|
||||
>>> G.add_edge(0, 1, key="a")
|
||||
'a'
|
||||
>>> G.has_edge(0, 1, key="a") # specify key
|
||||
True
|
||||
>>> e = (0, 1, "a")
|
||||
>>> G.has_edge(*e) # e is a 3-tuple (u, v, 'a')
|
||||
True
|
||||
|
||||
The following syntax are equivalent:
|
||||
|
||||
>>> G.has_edge(0, 1)
|
||||
True
|
||||
>>> 1 in G[0] # though this gives :exc:`KeyError` if 0 not in G
|
||||
True
|
||||
|
||||
"""
|
||||
try:
|
||||
if key is None:
|
||||
return v in self._adj[u]
|
||||
else:
|
||||
return key in self._adj[u][v]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def edges(self):
|
||||
edges = list()
|
||||
seen = {}
|
||||
for n, nbrs in self._adj.items():
|
||||
for nbr, kd in nbrs.items():
|
||||
if nbr not in seen:
|
||||
for k, dd in kd.items():
|
||||
edges.append((n, nbr, k, dd))
|
||||
seen[n] = 1
|
||||
del seen
|
||||
return edges
|
||||
|
||||
def get_edge_data(self, u, v, key=None, default=None):
|
||||
"""Returns the attribute dictionary associated with edge (u, v).
|
||||
|
||||
This is identical to `G[u][v][key]` except the default is returned
|
||||
instead of an exception is the edge doesn't exist.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes
|
||||
|
||||
default : any Python object (default=None)
|
||||
Value to return if the edge (u, v) is not found.
|
||||
|
||||
key : hashable identifier, optional (default=None)
|
||||
Return data only for the edge with specified key.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edge_dict : dictionary
|
||||
The edge attribute dictionary.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph
|
||||
>>> key = G.add_edge(0, 1, key="a", weight=7)
|
||||
>>> G[0][1]["a"] # key='a'
|
||||
{'weight': 7}
|
||||
>>> G.edges[0, 1, "a"] # key='a'
|
||||
{'weight': 7}
|
||||
|
||||
Warning: we protect the graph data structure by making
|
||||
`G.edges` and `G[1][2]` read-only dict-like structures.
|
||||
However, you can assign values to attributes in e.g.
|
||||
`G.edges[1, 2, 'a']` or `G[1][2]['a']` using an additional
|
||||
bracket as shown next. You need to specify all edge info
|
||||
to assign to the edge data associated with an edge.
|
||||
|
||||
>>> G[0][1]["a"]["weight"] = 10
|
||||
>>> G.edges[0, 1, "a"]["weight"] = 10
|
||||
>>> G[0][1]["a"]["weight"]
|
||||
10
|
||||
>>> G.edges[1, 0, "a"]["weight"]
|
||||
10
|
||||
|
||||
>>> G = eg.MultiGraph() # or MultiDiGraph
|
||||
>>> G = eg.complete_graph(4, create_using=eg.MultiDiGraph)
|
||||
>>> G.get_edge_data(0, 1)
|
||||
{0: {}}
|
||||
>>> e = (0, 1)
|
||||
>>> G.get_edge_data(*e) # tuple form
|
||||
{0: {}}
|
||||
>>> G.get_edge_data("a", "b", default=0) # edge not in graph, return 0
|
||||
0
|
||||
"""
|
||||
try:
|
||||
if key is None:
|
||||
return self._adj[u][v]
|
||||
else:
|
||||
return self._adj[u][v][key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
@property
|
||||
def degree(self, weight="weight"):
|
||||
degree = dict()
|
||||
if weight is None:
|
||||
for n in self._nodes:
|
||||
nbrs = self._succ[n]
|
||||
deg = sum(len(keys) for keys in nbrs.values()) + (
|
||||
n in nbrs and len(nbrs[n])
|
||||
)
|
||||
degree[n] = deg
|
||||
else:
|
||||
for n in self._nodes:
|
||||
nbrs = self._succ[n]
|
||||
deg = sum(
|
||||
d.get(weight, 1)
|
||||
for key_dict in nbrs.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
if n in nbrs:
|
||||
deg += sum(d.get(weight, 1) for d in nbrs[n].values())
|
||||
degree[n] = deg
|
||||
|
||||
def is_multigraph(self):
|
||||
"""Returns True if graph is a multigraph, False otherwise."""
|
||||
return True
|
||||
|
||||
def is_directed(self):
|
||||
"""Returns True if graph is directed, False otherwise."""
|
||||
return False
|
||||
|
||||
def copy(self):
|
||||
"""Returns a copy of the graph.
|
||||
|
||||
The copy method by default returns an independent shallow copy
|
||||
of the graph and attributes. That is, if an attribute is a
|
||||
container, that container is shared by the original an the copy.
|
||||
Use Python's `copy.deepcopy` for new containers.
|
||||
|
||||
Notes
|
||||
-----
|
||||
All copies reproduce the graph structure, but data attributes
|
||||
may be handled in different ways. There are four types of copies
|
||||
of a graph that people might want.
|
||||
|
||||
Deepcopy -- A "deepcopy" copies the graph structure as well as
|
||||
all data attributes and any objects they might contain.
|
||||
The entire graph object is new so that changes in the copy
|
||||
do not affect the original object. (see Python's copy.deepcopy)
|
||||
|
||||
Data Reference (Shallow) -- For a shallow copy the graph structure
|
||||
is copied but the edge, node and graph attribute dicts are
|
||||
references to those in the original graph. This saves
|
||||
time and memory but could cause confusion if you change an attribute
|
||||
in one graph and it changes the attribute in the other.
|
||||
EasyGraph does not provide this level of shallow copy.
|
||||
|
||||
Independent Shallow -- This copy creates new independent attribute
|
||||
dicts and then does a shallow copy of the attributes. That is, any
|
||||
attributes that are containers are shared between the new graph
|
||||
and the original. This is exactly what `dict.copy()` provides.
|
||||
You can obtain this style copy using:
|
||||
|
||||
>>> G = eg.path_graph(5)
|
||||
>>> H = G.copy()
|
||||
>>> H = eg.Graph(G)
|
||||
>>> H = G.__class__(G)
|
||||
|
||||
Fresh Data -- For fresh data, the graph structure is copied while
|
||||
new empty data attribute dicts are created. The resulting graph
|
||||
is independent of the original and it has no edge, node or graph
|
||||
attributes. Fresh copies are not enabled. Instead use:
|
||||
|
||||
>>> H = G.__class__()
|
||||
>>> H.add_nodes_from(G)
|
||||
>>> H.add_edges_from(G.edges)
|
||||
|
||||
See the Python copy module for more information on shallow
|
||||
and deep copies, https://docs.python.org/3/library/copy.html.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : Graph
|
||||
A copy of the graph.
|
||||
|
||||
See Also
|
||||
--------
|
||||
to_directed: return a directed copy of the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> H = G.copy()
|
||||
|
||||
"""
|
||||
G = self.__class__()
|
||||
G.graph.update(self.graph)
|
||||
G.add_nodes_from((n, d.copy()) for n, d in self._node.items())
|
||||
G.add_edges_from(
|
||||
(u, v, key, datadict.copy())
|
||||
for u, nbrs in self._adj.items()
|
||||
for v, keydict in nbrs.items()
|
||||
for key, datadict in keydict.items()
|
||||
)
|
||||
return G
|
||||
|
||||
def to_directed(self):
|
||||
"""Returns a directed representation of the graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : MultiDiGraph
|
||||
A directed graph with the same name, same nodes, and with
|
||||
each edge (u, v, data) replaced by two directed edges
|
||||
(u, v, data) and (v, u, data).
|
||||
|
||||
Notes
|
||||
-----
|
||||
This returns a "deepcopy" of the edge, node, and
|
||||
graph attributes which attempts to completely copy
|
||||
all of the data and references.
|
||||
|
||||
This is in contrast to the similar D=DiGraph(G) which returns a
|
||||
shallow copy of the data.
|
||||
|
||||
See the Python copy module for more information on shallow
|
||||
and deep copies, https://docs.python.org/3/library/copy.html.
|
||||
|
||||
Warning: If you have subclassed MultiGraph to use dict-like objects
|
||||
in the data structure, those changes do not transfer to the
|
||||
MultiDiGraph created by this method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or MultiGraph, etc
|
||||
>>> G.add_edge(0, 1)
|
||||
>>> H = G.to_directed()
|
||||
>>> list(H.edges)
|
||||
[(0, 1), (1, 0)]
|
||||
|
||||
If already directed, return a (deep) copy
|
||||
|
||||
>>> G = eg.DiGraph() # or MultiDiGraph, etc
|
||||
>>> G.add_edge(0, 1)
|
||||
>>> H = G.to_directed()
|
||||
>>> list(H.edges)
|
||||
[(0, 1)]
|
||||
"""
|
||||
G = eg.MultiDiGraph()
|
||||
G.graph.update(deepcopy(self.graph))
|
||||
G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
|
||||
G.add_edges_from(
|
||||
(u, v, key, deepcopy(datadict))
|
||||
for u, nbrs in self.adj.items()
|
||||
for v, keydict in nbrs.items()
|
||||
for key, datadict in keydict.items()
|
||||
)
|
||||
return G
|
||||
|
||||
def number_of_edges(self, u=None, v=None):
|
||||
"""Returns the number of edges between two nodes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u, v : nodes, optional (Gefault=all edges)
|
||||
If u and v are specified, return the number of edges between
|
||||
u and v. Otherwise return the total number of all edges.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nedges : int
|
||||
The number of edges in the graph. If nodes `u` and `v` are
|
||||
specified return the number of edges between those nodes. If
|
||||
the graph is directed, this only returns the number of edges
|
||||
from `u` to `v`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
size
|
||||
|
||||
Examples
|
||||
--------
|
||||
For undirected multigraphs, this method counts the total number
|
||||
of edges in the graph::
|
||||
|
||||
>>> G = eg.MultiGraph()
|
||||
>>> G.add_edges_from([(0, 1), (0, 1), (1, 2)])
|
||||
[0, 1, 0]
|
||||
>>> G.number_of_edges()
|
||||
3
|
||||
|
||||
If you specify two nodes, this counts the total number of edges
|
||||
joining the two nodes::
|
||||
|
||||
>>> G.number_of_edges(0, 1)
|
||||
2
|
||||
|
||||
For directed multigraphs, this method can count the total number
|
||||
of directed edges from `u` to `v`::
|
||||
|
||||
>>> G = eg.MultiDiGraph()
|
||||
>>> G.add_edges_from([(0, 1), (0, 1), (1, 0)])
|
||||
[0, 1, 0]
|
||||
>>> G.number_of_edges(0, 1)
|
||||
2
|
||||
>>> G.number_of_edges(1, 0)
|
||||
1
|
||||
|
||||
"""
|
||||
if u is None:
|
||||
return self.size()
|
||||
try:
|
||||
edgedata = self._adj[u][v]
|
||||
except KeyError:
|
||||
return 0 # no such edge
|
||||
return len(edgedata)
|
||||
@@ -0,0 +1,447 @@
|
||||
from itertools import chain
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"set_edge_attributes",
|
||||
"add_path",
|
||||
"set_node_attributes",
|
||||
"selfloop_edges",
|
||||
"topological_sort",
|
||||
"number_of_selfloops",
|
||||
"density",
|
||||
]
|
||||
|
||||
|
||||
def set_edge_attributes(G, values, name=None):
|
||||
"""Sets edge attributes from a given value or dictionary of values.
|
||||
|
||||
.. Warning:: The call order of arguments `values` and `name`
|
||||
switched between v1.x & v2.x.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
|
||||
values : scalar value, dict-like
|
||||
What the edge attribute should be set to. If `values` is
|
||||
not a dictionary, then it is treated as a single attribute value
|
||||
that is then applied to every edge in `G`. This means that if
|
||||
you provide a mutable object, like a list, updates to that object
|
||||
will be reflected in the edge attribute for each edge. The attribute
|
||||
name will be `name`.
|
||||
|
||||
If `values` is a dict or a dict of dict, it should be keyed
|
||||
by edge tuple to either an attribute value or a dict of attribute
|
||||
key/value pairs used to update the edge's attributes.
|
||||
For multigraphs, the edge tuples must be of the form ``(u, v, key)``,
|
||||
where `u` and `v` are nodes and `key` is the edge key.
|
||||
For non-multigraphs, the keys must be tuples of the form ``(u, v)``.
|
||||
|
||||
name : string (optional, default=None)
|
||||
Name of the edge attribute to set if values is a scalar.
|
||||
|
||||
Examples
|
||||
--------
|
||||
After computing some property of the edges of a graph, you may want
|
||||
to assign a edge attribute to store the value of that property for
|
||||
each edge::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> bb = eg.edge_betweenness_centrality(G, normalized=False)
|
||||
>>> eg.set_edge_attributes(G, bb, "betweenness")
|
||||
>>> G.edges[1, 2]["betweenness"]
|
||||
2.0
|
||||
|
||||
If you provide a list as the second argument, updates to the list
|
||||
will be reflected in the edge attribute for each edge::
|
||||
|
||||
>>> labels = []
|
||||
>>> eg.set_edge_attributes(G, labels, "labels")
|
||||
>>> labels.append("foo")
|
||||
>>> G.edges[0, 1]["labels"]
|
||||
['foo']
|
||||
>>> G.edges[1, 2]["labels"]
|
||||
['foo']
|
||||
|
||||
If you provide a dictionary of dictionaries as the second argument,
|
||||
the entire dictionary will be used to update edge attributes::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> attrs = {(0, 1): {"attr1": 20, "attr2": "nothing"}, (1, 2): {"attr2": 3}}
|
||||
>>> eg.set_edge_attributes(G, attrs)
|
||||
>>> G[0][1]["attr1"]
|
||||
20
|
||||
>>> G[0][1]["attr2"]
|
||||
'nothing'
|
||||
>>> G[1][2]["attr2"]
|
||||
3
|
||||
|
||||
Note that if the dict contains edges that are not in `G`, they are
|
||||
silently ignored::
|
||||
|
||||
>>> G = eg.Graph([(0, 1)])
|
||||
>>> eg.set_edge_attributes(G, {(1, 2): {"weight": 2.0}})
|
||||
>>> (1, 2) in G.edges()
|
||||
False
|
||||
|
||||
"""
|
||||
if name is not None:
|
||||
# `values` does not contain attribute names
|
||||
try:
|
||||
# if `values` is a dict using `.items()` => {edge: value}
|
||||
if G.is_multigraph():
|
||||
for (u, v, key), value in values.items():
|
||||
try:
|
||||
G[u][v][key][name] = value
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for (u, v), value in values.items():
|
||||
try:
|
||||
G[u][v][name] = value
|
||||
except KeyError:
|
||||
pass
|
||||
except AttributeError:
|
||||
# treat `values` as a constant
|
||||
for u, v, data in G.edges:
|
||||
data[name] = values
|
||||
else:
|
||||
# `values` consists of doct-of-dict {edge: {attr: value}} shape
|
||||
if G.is_multigraph():
|
||||
for (u, v, key), d in values.items():
|
||||
try:
|
||||
G[u][v][key].update(d)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for (u, v), d in values.items():
|
||||
try:
|
||||
G[u][v].update(d)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def add_path(G_to_add_to, nodes_for_path, **attr):
|
||||
"""Add a path to the Graph G_to_add_to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G_to_add_to : graph
|
||||
A EasyGraph graph
|
||||
nodes_for_path : iterable container
|
||||
A container of nodes. A path will be constructed from
|
||||
the nodes (in order) and added to the graph.
|
||||
attr : keyword arguments, optional (default= no attributes)
|
||||
Attributes to add to every edge in path.
|
||||
|
||||
See Also
|
||||
--------
|
||||
add_star, add_cycle
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph()
|
||||
>>> eg.add_path(G, [0, 1, 2, 3])
|
||||
>>> eg.add_path(G, [10, 11, 12], weight=7)
|
||||
"""
|
||||
nlist = iter(nodes_for_path)
|
||||
try:
|
||||
first_node = next(nlist)
|
||||
except StopIteration:
|
||||
return
|
||||
G_to_add_to.add_node(first_node)
|
||||
G_to_add_to.add_edges_from(pairwise(chain((first_node,), nlist)), **attr)
|
||||
|
||||
|
||||
def set_node_attributes(G, values, name=None):
|
||||
"""Sets node attributes from a given value or dictionary of values.
|
||||
|
||||
.. Warning:: The call order of arguments `values` and `name`
|
||||
switched between v1.x & v2.x.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
|
||||
values : scalar value, dict-like
|
||||
What the node attribute should be set to. If `values` is
|
||||
not a dictionary, then it is treated as a single attribute value
|
||||
that is then applied to every node in `G`. This means that if
|
||||
you provide a mutable object, like a list, updates to that object
|
||||
will be reflected in the node attribute for every node.
|
||||
The attribute name will be `name`.
|
||||
|
||||
If `values` is a dict or a dict of dict, it should be keyed
|
||||
by node to either an attribute value or a dict of attribute key/value
|
||||
pairs used to update the node's attributes.
|
||||
|
||||
name : string (optional, default=None)
|
||||
Name of the node attribute to set if values is a scalar.
|
||||
|
||||
Examples
|
||||
--------
|
||||
After computing some property of the nodes of a graph, you may want
|
||||
to assign a node attribute to store the value of that property for
|
||||
each node::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> bb = eg.betweenness_centrality(G)
|
||||
>>> isinstance(bb, dict)
|
||||
True
|
||||
>>> eg.set_node_attributes(G, bb, "betweenness")
|
||||
>>> G.nodes[1]["betweenness"]
|
||||
1.0
|
||||
|
||||
If you provide a list as the second argument, updates to the list
|
||||
will be reflected in the node attribute for each node::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> labels = []
|
||||
>>> eg.set_node_attributes(G, labels, "labels")
|
||||
>>> labels.append("foo")
|
||||
>>> G.nodes[0]["labels"]
|
||||
['foo']
|
||||
>>> G.nodes[1]["labels"]
|
||||
['foo']
|
||||
>>> G.nodes[2]["labels"]
|
||||
['foo']
|
||||
|
||||
If you provide a dictionary of dictionaries as the second argument,
|
||||
the outer dictionary is assumed to be keyed by node to an inner
|
||||
dictionary of node attributes for that node::
|
||||
|
||||
>>> G = eg.path_graph(3)
|
||||
>>> attrs = {0: {"attr1": 20, "attr2": "nothing"}, 1: {"attr2": 3}}
|
||||
>>> eg.set_node_attributes(G, attrs)
|
||||
>>> G.nodes[0]["attr1"]
|
||||
20
|
||||
>>> G.nodes[0]["attr2"]
|
||||
'nothing'
|
||||
>>> G.nodes[1]["attr2"]
|
||||
3
|
||||
>>> G.nodes[2]
|
||||
{}
|
||||
|
||||
Note that if the dictionary contains nodes that are not in `G`, the
|
||||
values are silently ignored::
|
||||
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_node(0)
|
||||
>>> eg.set_node_attributes(G, {0: "red", 1: "blue"}, name="color")
|
||||
>>> G.nodes[0]["color"]
|
||||
'red'
|
||||
>>> 1 in G.nodes
|
||||
False
|
||||
|
||||
"""
|
||||
# Set node attributes based on type of `values`
|
||||
if name is not None: # `values` must not be a dict of dict
|
||||
try: # `values` is a dict
|
||||
for n, v in values.items():
|
||||
try:
|
||||
G.nodes[n][name] = values[n]
|
||||
except KeyError:
|
||||
pass
|
||||
except AttributeError: # `values` is a constant
|
||||
for n in G:
|
||||
G.nodes[n][name] = values
|
||||
else: # `values` must be dict of dict
|
||||
for n, d in values.items():
|
||||
try:
|
||||
G.nodes[n].update(d)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def topological_generations(G):
|
||||
if not G.is_directed():
|
||||
raise AssertionError("Topological sort not defined on undirected graphs.")
|
||||
indegree_map = {v: d for v, d in G.in_degree().items() if d > 0}
|
||||
zero_indegree = [v for v, d in G.in_degree().items() if d == 0]
|
||||
while zero_indegree:
|
||||
this_generation = zero_indegree
|
||||
zero_indegree = []
|
||||
for node in this_generation:
|
||||
if node not in G:
|
||||
raise RuntimeError("Graph changed during iteration")
|
||||
for child in G.neighbors(node):
|
||||
try:
|
||||
indegree_map[child] -= 1
|
||||
except KeyError as err:
|
||||
raise RuntimeError("Graph changed during iteration") from err
|
||||
if indegree_map[child] == 0:
|
||||
zero_indegree.append(child)
|
||||
del indegree_map[child]
|
||||
yield this_generation
|
||||
|
||||
if indegree_map:
|
||||
raise AssertionError("Graph contains a cycle or graph changed during iteration")
|
||||
|
||||
|
||||
def topological_sort(G):
|
||||
for generation in topological_generations(G):
|
||||
yield from generation
|
||||
|
||||
|
||||
def number_of_selfloops(G):
|
||||
"""Returns the number of selfloop edges.
|
||||
|
||||
A selfloop edge has the same node at both ends.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nloops : int
|
||||
The number of selfloops.
|
||||
|
||||
See Also
|
||||
--------
|
||||
nodes_with_selfloops, selfloop_edges
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G.add_edge(1, 1)
|
||||
>>> G.add_edge(1, 2)
|
||||
>>> eg.number_of_selfloops(G)
|
||||
1
|
||||
"""
|
||||
return sum(1 for _ in eg.selfloop_edges(G))
|
||||
|
||||
|
||||
def selfloop_edges(G, data=False, keys=False, default=None):
|
||||
"""Returns an iterator over selfloop edges.
|
||||
|
||||
A selfloop edge has the same node at both ends.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph.
|
||||
data : string or bool, optional (default=False)
|
||||
Return selfloop edges as two tuples (u, v) (data=False)
|
||||
or three-tuples (u, v, datadict) (data=True)
|
||||
or three-tuples (u, v, datavalue) (data='attrname')
|
||||
keys : bool, optional (default=False)
|
||||
If True, return edge keys with each edge.
|
||||
default : value, optional (default=None)
|
||||
Value used for edges that don't have the requested attribute.
|
||||
Only relevant if data is not True or False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edgeiter : iterator over edge tuples
|
||||
An iterator over all selfloop edges.
|
||||
|
||||
See Also
|
||||
--------
|
||||
nodes_with_selfloops, number_of_selfloops
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.MultiGraph() # or Graph, DiGraph, MultiDiGraph, etc
|
||||
>>> ekey = G.add_edge(1, 1)
|
||||
>>> ekey = G.add_edge(1, 2)
|
||||
>>> list(eg.selfloop_edges(G))
|
||||
[(1, 1)]
|
||||
>>> list(eg.selfloop_edges(G, data=True))
|
||||
[(1, 1, {})]
|
||||
>>> list(eg.selfloop_edges(G, keys=True))
|
||||
[(1, 1, 0)]
|
||||
>>> list(eg.selfloop_edges(G, keys=True, data=True))
|
||||
[(1, 1, 0, {})]
|
||||
"""
|
||||
if data is True:
|
||||
if G.is_multigraph():
|
||||
if keys is True:
|
||||
return (
|
||||
(n, n, k, d)
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for k, d in nbrs[n].items()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(n, n, d)
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for d in nbrs[n].values()
|
||||
)
|
||||
else:
|
||||
return ((n, n, nbrs[n]) for n, nbrs in G.adj.items() if n in nbrs)
|
||||
elif data is not False:
|
||||
if G.is_multigraph():
|
||||
if keys is True:
|
||||
return (
|
||||
(n, n, k, d.get(data, default))
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for k, d in nbrs[n].items()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(n, n, d.get(data, default))
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for d in nbrs[n].values()
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(n, n, nbrs[n].get(data, default))
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
)
|
||||
else:
|
||||
if G.is_multigraph():
|
||||
if keys is True:
|
||||
return (
|
||||
(n, n, k) for n, nbrs in G.adj.items() if n in nbrs for k in nbrs[n]
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(n, n)
|
||||
for n, nbrs in G.adj.items()
|
||||
if n in nbrs
|
||||
for i in range(len(nbrs[n])) # for easy edge removal (#4068)
|
||||
)
|
||||
else:
|
||||
return ((n, n) for n, nbrs in G.adj.items() if n in nbrs)
|
||||
|
||||
|
||||
@hybrid("cpp_density")
|
||||
def density(G):
|
||||
r"""Returns the density of a graph.
|
||||
|
||||
The density for undirected graphs is
|
||||
|
||||
.. math::
|
||||
|
||||
d = \frac{2m}{n(n-1)},
|
||||
|
||||
and for directed graphs is
|
||||
|
||||
.. math::
|
||||
|
||||
d = \frac{m}{n(n-1)},
|
||||
|
||||
where `n` is the number of nodes and `m` is the number of edges in `G`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The density is 0 for a graph without edges and 1 for a complete graph.
|
||||
The density of multigraphs can be higher than 1.
|
||||
|
||||
Self loops are counted in the total number of edges so graphs with self
|
||||
loops can have density higher than 1.
|
||||
"""
|
||||
n = G.number_of_nodes()
|
||||
m = G.number_of_edges()
|
||||
if m == 0 or n <= 1:
|
||||
return 0
|
||||
d = m / (n * (n - 1))
|
||||
if not G.is_directed():
|
||||
d *= 2
|
||||
return d
|
||||
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
print(
|
||||
os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "../cpp_easygraph")
|
||||
)
|
||||
)
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
import easygraph.classes as cls # Spend 4.9s on importing this damn big lib.
|
||||
|
||||
|
||||
def test_iter():
|
||||
g = eg.Graph()
|
||||
# repeated endings test
|
||||
g.add_edge(None, None) # 1
|
||||
g.add_edge(True, False)
|
||||
|
||||
g.add_edge(0b1000, 100)
|
||||
print(g.edges)
|
||||
|
||||
|
||||
test_iter()
|
||||
@@ -0,0 +1,145 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
pd = pytest.importorskip("pandas")
|
||||
sp = pytest.importorskip("scipy")
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.misc import *
|
||||
|
||||
|
||||
class TestConvertNumpyArray:
|
||||
def setup_method(self):
|
||||
self.G1 = eg.complete_graph(5)
|
||||
|
||||
def assert_equal(self, G1, G2):
|
||||
assert nodes_equal(G1.nodes, G2.nodes)
|
||||
assert edges_equal(G1.edges, G2.edges, need_data=False)
|
||||
|
||||
def identity_conversion(self, G, A, create_using):
|
||||
assert A.sum() > 0
|
||||
GG = eg.from_numpy_array(A, create_using=create_using)
|
||||
self.assert_equal(G, GG)
|
||||
GW = eg.to_easygraph_graph(A, create_using=create_using)
|
||||
self.assert_equal(G, GW)
|
||||
|
||||
def test_identity_graph_array(self):
|
||||
A = eg.to_numpy_array(self.G1)
|
||||
self.identity_conversion(self.G1, A, eg.Graph())
|
||||
|
||||
|
||||
class TestConvertPandas:
|
||||
def setup_method(self):
|
||||
self.rng = np.random.RandomState(seed=5)
|
||||
ints = self.rng.randint(1, 11, size=(3, 2))
|
||||
a = ["A", "B", "C"]
|
||||
b = ["D", "A", "E"]
|
||||
df = pd.DataFrame(ints, columns=["weight", "cost"])
|
||||
df[0] = a
|
||||
df["b"] = b
|
||||
self.df = df
|
||||
|
||||
mdf = pd.DataFrame([[4, 16, "A", "D"]], columns=["weight", "cost", 0, "b"])
|
||||
self.mdf = pd.concat([df, mdf])
|
||||
|
||||
def assert_equal(self, G1, G2):
|
||||
assert nodes_equal(G1.nodes, G2.nodes)
|
||||
assert edges_equal(G1.edges, G2.edges, need_data=False)
|
||||
|
||||
def test_from_edgelist_multi_attr(self):
|
||||
Gtrue = eg.Graph(
|
||||
[
|
||||
("E", "C", {"cost": 9, "weight": 10}),
|
||||
("B", "A", {"cost": 1, "weight": 7}),
|
||||
("A", "D", {"cost": 7, "weight": 4}),
|
||||
]
|
||||
)
|
||||
G = eg.from_pandas_edgelist(self.df, 0, "b", ["weight", "cost"])
|
||||
self.assert_equal(G, Gtrue)
|
||||
|
||||
def test_from_adjacency(self):
|
||||
Gtrue = eg.DiGraph([("A", "B"), ("B", "C")])
|
||||
data = {
|
||||
"A": {"A": 0, "B": 0, "C": 0},
|
||||
"B": {"A": 1, "B": 0, "C": 0},
|
||||
"C": {"A": 0, "B": 1, "C": 0},
|
||||
}
|
||||
dftrue = pd.DataFrame(data, dtype=np.intp)
|
||||
df = dftrue[["A", "C", "B"]]
|
||||
G = eg.from_pandas_adjacency(df, create_using=eg.DiGraph())
|
||||
self.assert_equal(G, Gtrue)
|
||||
|
||||
|
||||
class TestConvertScipy:
|
||||
def setup_method(self):
|
||||
self.G1 = eg.complete_graph(3)
|
||||
|
||||
def assert_equal(self, G1, G2):
|
||||
assert nodes_equal(G1.nodes, G2.nodes)
|
||||
assert edges_equal(G1.edges, G2.edges, need_data=False)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 8), reason="requires python3.8 or higher"
|
||||
)
|
||||
def test_from_scipy(self):
|
||||
data = sp.sparse.csr_matrix([[0, 1, 1], [1, 0, 1], [1, 1, 0]])
|
||||
G = eg.from_scipy_sparse_matrix(data)
|
||||
self.assert_equal(self.G1, G)
|
||||
|
||||
|
||||
def test_from_edgelist():
|
||||
edgelist = [(0, 1), (1, 2)]
|
||||
G = eg.from_edgelist(edgelist)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)]
|
||||
|
||||
|
||||
def test_from_dict_of_lists():
|
||||
d = {0: [1], 1: [2]}
|
||||
G = eg.to_easygraph_graph(d)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)]
|
||||
|
||||
|
||||
def test_from_dict_of_dicts():
|
||||
d = {0: {1: {}}, 1: {2: {}}}
|
||||
G = eg.to_easygraph_graph(d)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)]
|
||||
|
||||
|
||||
def test_from_numpy_array():
|
||||
G = eg.complete_graph(3)
|
||||
A = eg.to_numpy_array(G)
|
||||
G2 = eg.from_numpy_array(A)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == sorted(
|
||||
(u, v) for u, v, _ in G2.edges
|
||||
)
|
||||
|
||||
|
||||
def test_from_pandas_edgelist():
|
||||
df = pd.DataFrame({"source": [0, 1], "target": [1, 2], "weight": [0.5, 0.7]})
|
||||
G = eg.from_pandas_edgelist(df, source="source", target="target", edge_attr=True)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [(0, 1), (1, 2)]
|
||||
|
||||
|
||||
def test_from_pandas_adjacency():
|
||||
df = pd.DataFrame([[0, 1], [1, 0]], columns=["A", "B"], index=["A", "B"])
|
||||
G = eg.from_pandas_adjacency(df)
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == [("A", "B")]
|
||||
|
||||
|
||||
def test_from_scipy_sparse_matrix():
|
||||
mat = sp.sparse.csr_matrix([[0, 1, 0], [1, 0, 1], [0, 1, 0]])
|
||||
G = eg.from_scipy_sparse_matrix(mat)
|
||||
expected_edges = [(0, 1), (1, 2)]
|
||||
assert sorted((u, v) for u, v, _ in G.edges) == expected_edges
|
||||
|
||||
|
||||
def test_invalid_dict_type():
|
||||
class NotGraph:
|
||||
pass
|
||||
|
||||
with pytest.raises(eg.EasyGraphError):
|
||||
eg.to_easygraph_graph(NotGraph())
|
||||
@@ -0,0 +1,97 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from easygraph import DiGraph
|
||||
|
||||
|
||||
class TestDiGraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = DiGraph()
|
||||
|
||||
def test_add_node_and_exists(self):
|
||||
self.G.add_node("A")
|
||||
self.assertTrue(self.G.has_node("A"))
|
||||
self.assertIn("A", self.G.nodes)
|
||||
|
||||
def test_add_nodes_with_attrs(self):
|
||||
self.G.add_nodes(["B", "C"], nodes_attr=[{"age": 30}, {"age": 40}])
|
||||
self.assertEqual(self.G.nodes["B"]["age"], 30)
|
||||
self.assertEqual(self.G.nodes["C"]["age"], 40)
|
||||
|
||||
def test_add_edge_and_attrs(self):
|
||||
self.G.add_edge("A", "B", weight=5)
|
||||
self.assertTrue(self.G.has_edge("A", "B"))
|
||||
self.assertEqual(self.G.adj["A"]["B"]["weight"], 5)
|
||||
|
||||
def test_add_edges_with_attrs(self):
|
||||
self.G.add_edges([("B", "C"), ("C", "D")], edges_attr=[{"w": 1}, {"w": 2}])
|
||||
self.assertEqual(self.G.adj["B"]["C"]["w"], 1)
|
||||
self.assertEqual(self.G.adj["C"]["D"]["w"], 2)
|
||||
|
||||
def test_remove_node_and_edges(self):
|
||||
self.G.add_edges([("X", "Y"), ("Y", "Z")])
|
||||
self.G.remove_node("Y")
|
||||
self.assertFalse("Y" in self.G.nodes)
|
||||
self.assertFalse(self.G.has_edge("Y", "Z"))
|
||||
|
||||
def test_remove_edge(self):
|
||||
self.G.add_edge("M", "N")
|
||||
self.G.remove_edge("M", "N")
|
||||
self.assertFalse(self.G.has_edge("M", "N"))
|
||||
|
||||
def test_degrees(self):
|
||||
self.G.add_edges(
|
||||
[("A", "B"), ("C", "B")], edges_attr=[{"weight": 3}, {"weight": 2}]
|
||||
)
|
||||
|
||||
in_degrees = self.G.in_degree(weight="weight")
|
||||
out_degrees = self.G.out_degree(weight="weight")
|
||||
degrees = self.G.degree(weight="weight")
|
||||
|
||||
self.assertEqual(in_degrees["B"], 5)
|
||||
self.assertEqual(out_degrees["A"], 3)
|
||||
self.assertEqual(degrees["B"], 5)
|
||||
|
||||
def test_neighbors_and_preds(self):
|
||||
self.G.add_edges([("P", "Q"), ("R", "P")])
|
||||
self.assertIn("Q", list(self.G.neighbors("P")))
|
||||
self.assertIn("R", list(self.G.predecessors("P")))
|
||||
all_n = list(self.G.all_neighbors("P"))
|
||||
self.assertIn("Q", all_n)
|
||||
self.assertIn("R", all_n)
|
||||
|
||||
def test_size_and_num_edges_nodes(self):
|
||||
self.G.add_edges([("X", "Y"), ("Y", "Z")])
|
||||
self.assertEqual(self.G.size(), 2)
|
||||
self.assertEqual(self.G.number_of_edges(), 2)
|
||||
self.assertEqual(self.G.number_of_nodes(), 3)
|
||||
|
||||
def test_subgraph_and_ego(self):
|
||||
self.G.add_edges([("A", "B"), ("B", "C"), ("C", "D")])
|
||||
sub = self.G.nodes_subgraph(["A", "B", "C"])
|
||||
self.assertTrue(sub.has_edge("A", "B"))
|
||||
self.assertFalse(sub.has_edge("C", "D"))
|
||||
ego = self.G.ego_subgraph("B")
|
||||
self.assertIn("A", ego.nodes or [])
|
||||
self.assertIn("C", ego.nodes or [])
|
||||
|
||||
def test_to_index_node_graph(self):
|
||||
self.G.add_edges([("foo", "bar"), ("bar", "baz")])
|
||||
G2, node2idx, idx2node = self.G.to_index_node_graph()
|
||||
self.assertEqual(len(G2.nodes), 3)
|
||||
self.assertEqual(node2idx["foo"], 0)
|
||||
self.assertEqual(idx2node[0], "foo")
|
||||
|
||||
def test_copy(self):
|
||||
self.G.add_edge("copyA", "copyB", weight=42)
|
||||
G_copy = self.G.copy()
|
||||
self.assertEqual(G_copy.adj["copyA"]["copyB"]["weight"], 42)
|
||||
|
||||
def test_file_add_edges(self):
|
||||
fname = "temp_edges.txt"
|
||||
with open(fname, "w") as f:
|
||||
f.write("1 2 3.5\n2 3 4.5\n")
|
||||
self.G.add_edges_from_file(fname, weighted=True)
|
||||
os.remove(fname)
|
||||
self.assertEqual(self.G.adj["1"]["2"]["weight"], 3.5)
|
||||
self.assertEqual(self.G.adj["2"]["3"]["weight"], 4.5)
|
||||
@@ -0,0 +1,112 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
# sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),'..', '..')))
|
||||
import easygraph as eg # Spend 4.9s on importing this damn big lib.
|
||||
|
||||
|
||||
"""
|
||||
def test_iter():
|
||||
g = eg.Graph()
|
||||
# tests of corner cases
|
||||
|
||||
g.add_edge(0, 0)
|
||||
g.add_edge(True, False)
|
||||
g.add_edge(False, 1)
|
||||
g.add_edge(0b1000, 0x00a, edge_attr={"age": 19, "gender": "Male"})
|
||||
# g.add_edge(None, None) # this shall result in an AssertionError
|
||||
# g.add_edge(None, 1) # this shall result in an AssertionError
|
||||
# g.add_edge(1, None) # this shall result in an AssertionError
|
||||
|
||||
# g.add_edges(None) # Triggers a TypeError saying that len() is not applicable to None
|
||||
g.add_edges([(True, False), ("Beijing National", "Day School")], [{}, {"Rating": 100}])
|
||||
g.add_node("FuDan Univ", node_attr={"faculty": 10000}) # 1.
|
||||
g.add_edge("Beijing National", "FuDan Univ")
|
||||
# g.add_node([]) # this shall result in an unhashable error
|
||||
g.add_node('Jack', node_attr={
|
||||
'age': 10,
|
||||
'gender': 'M'
|
||||
})
|
||||
# g.remove_node("Beijing National")
|
||||
g.remove_edges([('Day School', 'Beijing National')])
|
||||
# g.add_edges_from()
|
||||
|
||||
print(g.add_extra_selfloop())
|
||||
|
||||
|
||||
g.nbr_v()
|
||||
g.nbunch_iter()
|
||||
g.from_hypergraph_hypergcn()
|
||||
# print(g._adj[8].get(10))
|
||||
print(g.edges)
|
||||
print(g.nodes)
|
||||
|
||||
|
||||
test_iter()
|
||||
"""
|
||||
|
||||
from easygraph.datasets import get_graph_karateclub
|
||||
|
||||
|
||||
G = get_graph_karateclub()
|
||||
# Calculate five shs(Structural Hole Spanners) in G
|
||||
shs = eg.common_greedy(G, 5)
|
||||
# Draw the Graph, and the shs is marked by a red star
|
||||
eg.draw_SHS_center(G, shs)
|
||||
# Draw CDF curves of "Number of Followers" of SH spanners and ordinary users in G.
|
||||
eg.plot_Followers(G, shs)
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
G = eg.Graph()
|
||||
G.add_edge(1, 2) # Add a single edge
|
||||
print(G.edges)
|
||||
|
||||
G.add_edges([(2, 3), (1, 3), (3, 4), (4, 5), ((1, 2), (3, 4))]) # Add edges
|
||||
print(G.edges)
|
||||
|
||||
|
||||
G.add_node("hello world")
|
||||
G.add_node("Jack", node_attr={"age": 10, "gender": "M"})
|
||||
print(G.nodes)
|
||||
|
||||
# G.remove_nodes(['hello world','Tom','Lily','a','b'])#remove edges
|
||||
G.remove_nodes(["hello world"])
|
||||
print(G.nodes)
|
||||
|
||||
G.remove_edge(4, 5)
|
||||
print(G.edges)
|
||||
|
||||
print(len(G)) # __len__(self)
|
||||
for x in G: # __iter__(self)
|
||||
print(x)
|
||||
print(G[1]) # return list(self._adj[node].keys()) __contains__ __getitem__
|
||||
|
||||
for neighbor in G.neighbors(node=2):
|
||||
print(neighbor)
|
||||
|
||||
G.add_edges(
|
||||
[(1, 2), (2, 3), (1, 3), (3, 4), (4, 5)],
|
||||
edges_attr=[
|
||||
{"weight": 20},
|
||||
{"weight": 10},
|
||||
{"weight": 15},
|
||||
{"weight": 8},
|
||||
{"weight": 12},
|
||||
],
|
||||
) # add weighted edges
|
||||
G.add_node(6)
|
||||
print(G.edges)
|
||||
|
||||
print(G.degree())
|
||||
print(G.degree(weight="weight"))
|
||||
|
||||
G_index_graph, index_of_node, node_of_index = G.to_index_node_graph()
|
||||
print(G_index_graph.adj)
|
||||
|
||||
G1 = G.copy()
|
||||
print(G1.adj)
|
||||
|
||||
print(eg.effective_size(G))
|
||||
@@ -0,0 +1,122 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestEasyGraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.Graph()
|
||||
|
||||
def test_add_single_node(self):
|
||||
self.G.add_node(1)
|
||||
self.assertIn(1, self.G.nodes)
|
||||
|
||||
def test_add_multiple_nodes(self):
|
||||
self.G.add_nodes([2, 3, 4])
|
||||
for node in [2, 3, 4]:
|
||||
self.assertIn(node, self.G.nodes)
|
||||
|
||||
def test_add_node_with_attributes(self):
|
||||
self.G.add_node("node", color="red")
|
||||
self.assertEqual(self.G.nodes["node"]["color"], "red")
|
||||
|
||||
def test_add_single_edge(self):
|
||||
self.G.add_edge(1, 2)
|
||||
self.assertTrue(self.G.has_edge(1, 2))
|
||||
self.assertTrue(self.G.has_edge(2, 1))
|
||||
|
||||
def test_add_edge_with_weight(self):
|
||||
self.G.add_edge("a", "b", weight=10)
|
||||
self.assertEqual(self.G["a"]["b"]["weight"], 10)
|
||||
|
||||
def test_add_edges(self):
|
||||
self.G.add_edges([(1, 2), (2, 3)], edges_attr=[{"weight": 5}, {"weight": 6}])
|
||||
self.assertEqual(self.G[1][2]["weight"], 5)
|
||||
self.assertEqual(self.G[2][3]["weight"], 6)
|
||||
|
||||
def test_remove_node(self):
|
||||
self.G.add_node(10)
|
||||
self.G.remove_node(10)
|
||||
self.assertNotIn(10, self.G.nodes)
|
||||
|
||||
def test_remove_edge(self):
|
||||
self.G.add_edge(1, 2)
|
||||
self.G.remove_edge(1, 2)
|
||||
self.assertFalse(self.G.has_edge(1, 2))
|
||||
|
||||
def test_neighbors(self):
|
||||
self.G.add_edges([(1, 2), (1, 3)])
|
||||
neighbors = list(self.G.neighbors(1))
|
||||
self.assertIn(2, neighbors)
|
||||
self.assertIn(3, neighbors)
|
||||
|
||||
def test_subgraph(self):
|
||||
self.G.add_edges([(1, 2), (2, 3), (3, 4)])
|
||||
subG = self.G.nodes_subgraph([2, 3])
|
||||
self.assertIn(2, subG.nodes)
|
||||
self.assertIn(3, subG.nodes)
|
||||
self.assertTrue(subG.has_edge(2, 3))
|
||||
self.assertFalse(subG.has_edge(3, 4))
|
||||
|
||||
def test_ego_subgraph(self):
|
||||
self.G.add_edges([(1, 2), (2, 3), (2, 4)])
|
||||
ego = self.G.ego_subgraph(2)
|
||||
self.assertIn(2, ego.nodes)
|
||||
self.assertIn(1, ego.nodes)
|
||||
self.assertIn(3, ego.nodes)
|
||||
self.assertIn(4, ego.nodes)
|
||||
|
||||
def test_to_index_node_graph(self):
|
||||
self.G.add_edges([("a", "b"), ("b", "c")])
|
||||
G_index, index_of_node, node_of_index = self.G.to_index_node_graph()
|
||||
self.assertEqual(len(G_index.nodes), 3)
|
||||
self.assertTrue(all(isinstance(k, int) for k in G_index.nodes))
|
||||
|
||||
def test_directed_conversion(self):
|
||||
self.G.add_edge(1, 2)
|
||||
H = self.G.to_directed()
|
||||
self.assertTrue(H.is_directed())
|
||||
self.assertTrue(H.has_edge(1, 2))
|
||||
self.assertTrue(H.has_edge(2, 1))
|
||||
|
||||
def test_clone_graph(self):
|
||||
self.G.add_edges([(1, 2), (2, 3)])
|
||||
G_clone = self.G.copy()
|
||||
self.assertTrue(G_clone.has_edge(1, 2))
|
||||
self.assertTrue(G_clone.has_edge(2, 3))
|
||||
|
||||
def test_degree(self):
|
||||
self.G.add_edge(1, 2, weight=5)
|
||||
deg = self.G.degree()
|
||||
self.assertEqual(deg[1], 5)
|
||||
self.assertEqual(deg[2], 5)
|
||||
|
||||
def test_size(self):
|
||||
self.G.add_edges([(1, 2), (2, 3)])
|
||||
self.assertEqual(self.G.size(), 2)
|
||||
|
||||
def test_edge_weight_default(self):
|
||||
self.G.add_edge(4, 5)
|
||||
self.assertEqual(self.G[4][5].get("weight", 1), 1)
|
||||
|
||||
def test_node_index_mappings(self):
|
||||
self.G.add_nodes([10, 20, 30])
|
||||
index2node = self.G.index2node
|
||||
node_index = self.G.node_index
|
||||
for i, node in index2node.items():
|
||||
self.assertEqual(node_index[node], i)
|
||||
|
||||
def test_graph_order(self):
|
||||
self.G.add_nodes([1, 2, 3])
|
||||
self.assertEqual(self.G.order(), 3)
|
||||
|
||||
def test_graph_size_with_weight(self):
|
||||
self.G.add_edges([(1, 2), (2, 3)], edges_attr=[{"weight": 4}, {"weight": 6}])
|
||||
self.assertEqual(self.G.size(weight="weight"), 10.0)
|
||||
|
||||
def test_clear_cache(self):
|
||||
self.G.add_edge(1, 2)
|
||||
_ = self.G.edges
|
||||
self.assertIn("edge", self.G.cache)
|
||||
self.G._clear_cache()
|
||||
self.assertEqual(len(self.G.cache), 0)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
def setUp(self):
|
||||
edges = [(1, 2), (2, 3), ("String", "Bool"), (2, 1), ((1, 2), (3, 4))]
|
||||
self.g = eg.MultiDiGraph(edges)
|
||||
|
||||
def test_add_edge(self):
|
||||
self.g.add_edge("from_Beijing", "to_California", key=3, attr=None)
|
||||
print(self.g.edges)
|
||||
|
||||
def test_remove_edge(self):
|
||||
self.g.add_edge("from_Beijing", "to_California", key=3, attr=None)
|
||||
self.g.remove_edge("from_Beijing", "to_California")
|
||||
print(self.g.edges)
|
||||
|
||||
def test_degree(self):
|
||||
print(self.g.degree)
|
||||
print(self.g.in_degree)
|
||||
print(self.g.out_degree)
|
||||
|
||||
def test_reverse(self):
|
||||
# error with _succ
|
||||
print(self.g.reverse(copy=True).edges)
|
||||
# print(self.g.reverse(copy=False).edges)
|
||||
|
||||
def test_attributes(self):
|
||||
print(self.g.edges)
|
||||
print(self.g.in_edges)
|
||||
|
||||
|
||||
class TestMultiDiGraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.MultiDiGraph()
|
||||
|
||||
def test_add_edge_without_key(self):
|
||||
key1 = self.G.add_edge("A", "B", weight=1)
|
||||
key2 = self.G.add_edge("A", "B", weight=2)
|
||||
self.assertNotEqual(key1, key2)
|
||||
self.assertEqual(len(self.G._adj["A"]["B"]), 2)
|
||||
|
||||
def test_add_edge_with_key(self):
|
||||
key = self.G.add_edge("A", "B", key="mykey", weight=3)
|
||||
self.assertEqual(key, "mykey")
|
||||
self.assertEqual(self.G._adj["A"]["B"]["mykey"]["weight"], 3)
|
||||
|
||||
def test_edge_attributes_update(self):
|
||||
self.G.add_edge("X", "Y", key=1, color="red")
|
||||
self.G.add_edge("X", "Y", key=1, shape="circle")
|
||||
self.assertEqual(self.G._adj["X"]["Y"][1]["color"], "red")
|
||||
self.assertEqual(self.G._adj["X"]["Y"][1]["shape"], "circle")
|
||||
|
||||
def test_remove_edge_by_key(self):
|
||||
self.G.add_edge("A", "B", key="k1")
|
||||
self.G.add_edge("A", "B", key="k2")
|
||||
self.G.remove_edge("A", "B", key="k1")
|
||||
self.assertIn("k2", self.G._adj["A"]["B"])
|
||||
self.assertNotIn("k1", self.G._adj["A"]["B"])
|
||||
|
||||
def test_remove_edge_without_key(self):
|
||||
self.G.add_edge("A", "B", key="auto1")
|
||||
self.G.add_edge("A", "B", key="auto2")
|
||||
self.G.remove_edge("A", "B")
|
||||
# Only one of the keys should remain
|
||||
self.assertEqual(len(self.G._adj["A"]["B"]), 1)
|
||||
|
||||
def test_remove_nonexistent_edge_raises(self):
|
||||
with self.assertRaises(eg.EasyGraphError):
|
||||
self.G.remove_edge("X", "Y", key="doesnotexist")
|
||||
|
||||
def test_edges_property(self):
|
||||
self.G.add_edge("U", "V", key="k", weight=5)
|
||||
edges = self.G.edges
|
||||
self.assertIn(("U", "V", "k", {"weight": 5}), edges)
|
||||
|
||||
def test_in_out_degree(self):
|
||||
self.G.add_edge("A", "B", weight=3)
|
||||
self.G.add_edge("C", "B", weight=2)
|
||||
|
||||
in_deg = {}
|
||||
for n in self.G._node:
|
||||
preds = self.G._pred[n]
|
||||
in_deg[n] = sum(
|
||||
d.get("weight", 1)
|
||||
for key_dict in preds.values()
|
||||
for d in key_dict.values()
|
||||
)
|
||||
|
||||
self.assertEqual(in_deg["B"], 5)
|
||||
|
||||
def test_to_undirected(self):
|
||||
self.G.add_edge("A", "B", key="k", weight=10)
|
||||
UG = self.G.to_undirected()
|
||||
self.assertTrue(UG.has_edge("A", "B"))
|
||||
self.assertEqual(UG["A"]["B"]["k"]["weight"], 10)
|
||||
|
||||
def test_reverse_graph(self):
|
||||
self.G.add_edge("A", "B", key="k", data=99)
|
||||
RG = self.G.reverse()
|
||||
self.assertTrue(RG.has_edge("B", "A"))
|
||||
self.assertEqual(RG["B"]["A"]["k"]["data"], 99)
|
||||
|
||||
def test_is_multigraph_and_directed(self):
|
||||
self.assertTrue(self.G.is_multigraph())
|
||||
self.assertTrue(self.G.is_directed())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
# test()
|
||||
@@ -0,0 +1,149 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class TestMultiGraph:
|
||||
def setup_method(self):
|
||||
self.Graph = eg.MultiGraph
|
||||
# build K3
|
||||
ed1, ed2, ed3 = ({0: {}}, {0: {}}, {0: {}})
|
||||
self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed1, 2: ed3}, 2: {0: ed2, 1: ed3}}
|
||||
self.k3edges = [(0, 1), (0, 2), (1, 2)]
|
||||
self.k3nodes = [0, 1, 2]
|
||||
self.K3 = self.Graph()
|
||||
self.K3._adj = self.k3adj
|
||||
self.K3._node = {}
|
||||
self.K3._node[0] = {}
|
||||
self.K3._node[1] = {}
|
||||
self.K3._node[2] = {}
|
||||
|
||||
def test_data_input(self):
|
||||
G = self.Graph({1: [2], 2: [1]}, name="test")
|
||||
assert G.name == "test"
|
||||
expected = [(1, {2: {0: {}}}), (2, {1: {0: {}}})]
|
||||
assert sorted(G.adj.items()) == expected
|
||||
|
||||
def test_has_edge(self):
|
||||
G = self.K3
|
||||
assert G.has_edge(0, 1)
|
||||
assert not G.has_edge(0, -1)
|
||||
assert G.has_edge(0, 1, 0)
|
||||
assert not G.has_edge(0, 1, 1)
|
||||
|
||||
def test_get_edge_data(self):
|
||||
G = self.K3
|
||||
assert G.get_edge_data(0, 1) == {0: {}}
|
||||
assert G[0][1] == {0: {}}
|
||||
assert G[0][1][0] == {}
|
||||
assert G.get_edge_data(10, 20) is None
|
||||
assert G.get_edge_data(0, 1, 0) == {}
|
||||
|
||||
def test_data_multigraph_input(self):
|
||||
# standard case with edge keys and edge data
|
||||
edata0 = dict(w=200, s="foo")
|
||||
edata1 = dict(w=201, s="bar")
|
||||
keydict = {0: edata0, 1: edata1}
|
||||
dododod = {"a": {"b": keydict}}
|
||||
|
||||
multiple_edge = [("a", "b", 0, edata0), ("a", "b", 1, edata1)]
|
||||
single_edge = [("a", "b", 0, keydict)]
|
||||
|
||||
G = self.Graph(dododod, multigraph_input=None)
|
||||
assert list(G.edges) == multiple_edge
|
||||
G = self.Graph(dododod, multigraph_input=False)
|
||||
assert list(G.edges) == single_edge
|
||||
|
||||
def test_remove_node(self):
|
||||
G = self.K3
|
||||
G.remove_node(0)
|
||||
assert G.adj == {1: {2: {0: {}}}, 2: {1: {0: {}}}}
|
||||
with pytest.raises(eg.EasyGraphError):
|
||||
G.remove_node(-1)
|
||||
|
||||
|
||||
class TestMultiGraphExtended:
|
||||
def test_add_multiple_edges_and_keys(self):
|
||||
G = eg.MultiGraph()
|
||||
k0 = G.add_edge(1, 2)
|
||||
k1 = G.add_edge(1, 2)
|
||||
assert k0 == 0
|
||||
assert k1 == 1
|
||||
assert G.number_of_edges(1, 2) == 2
|
||||
|
||||
def test_add_edge_with_key_and_attributes(self):
|
||||
G = eg.MultiGraph()
|
||||
k = G.add_edge(1, 2, key="custom", weight=3, label="test")
|
||||
assert k == "custom"
|
||||
assert G.get_edge_data(1, 2, "custom") == {"weight": 3, "label": "test"}
|
||||
|
||||
def test_add_edges_from_various_formats(self):
|
||||
G = eg.MultiGraph()
|
||||
edges = [
|
||||
(1, 2), # 2-tuple
|
||||
(2, 3, {"weight": 7}), # 3-tuple with attr
|
||||
(3, 4, "k1", {"color": "red"}), # 4-tuple
|
||||
]
|
||||
keys = G.add_edges_from(edges, capacity=100)
|
||||
assert len(keys) == 3
|
||||
assert G.get_edge_data(3, 4, "k1")["color"] == "red"
|
||||
assert G.get_edge_data(2, 3, 0)["capacity"] == 100
|
||||
|
||||
def test_remove_edge_with_key(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(1, 2, key="a")
|
||||
G.add_edge(1, 2, key="b")
|
||||
G.remove_edge(1, 2, key="a")
|
||||
assert not G.has_edge(1, 2, key="a")
|
||||
assert G.has_edge(1, 2, key="b")
|
||||
|
||||
def test_remove_edge_arbitrary(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(1, 2)
|
||||
G.add_edge(1, 2)
|
||||
G.remove_edge(1, 2)
|
||||
assert G.number_of_edges(1, 2) == 1
|
||||
|
||||
def test_remove_edges_from_mixed(self):
|
||||
G = eg.MultiGraph()
|
||||
keys = G.add_edges_from([(1, 2), (1, 2), (2, 3)])
|
||||
G.remove_edges_from([(1, 2), (2, 3)])
|
||||
assert G.number_of_edges(1, 2) == 1
|
||||
assert G.number_of_edges(2, 3) == 0
|
||||
|
||||
def test_to_directed_graph(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(0, 1, weight=10)
|
||||
D = G.to_directed()
|
||||
assert D.is_directed()
|
||||
assert D.has_edge(0, 1)
|
||||
assert D.has_edge(1, 0)
|
||||
assert D.get_edge_data(0, 1, 0)["weight"] == 10
|
||||
|
||||
def test_copy_graph(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(1, 2, key="x", weight=9)
|
||||
H = G.copy()
|
||||
assert H.get_edge_data(1, 2, "x") == {"weight": 9}
|
||||
assert H is not G
|
||||
assert H.get_edge_data(1, 2, "x") is not G.get_edge_data(1, 2, "x")
|
||||
|
||||
def test_has_edge_variants(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(1, 2)
|
||||
G.add_edge(1, 2, key="z")
|
||||
assert G.has_edge(1, 2)
|
||||
assert G.has_edge(1, 2, key="z")
|
||||
assert not G.has_edge(2, 1, key="nonexistent")
|
||||
|
||||
def test_get_edge_data_defaults(self):
|
||||
G = eg.MultiGraph()
|
||||
assert G.get_edge_data(10, 20) is None
|
||||
assert G.get_edge_data(10, 20, key="any", default="missing") == "missing"
|
||||
|
||||
def test_edge_property_returns_all_edges(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(0, 1, key=5, label="important")
|
||||
G.add_edge(1, 0, key=3, label="also important")
|
||||
edges = list(G.edges)
|
||||
assert any((0, 1, 5, {"label": "important"}) == e for e in edges)
|
||||
assert any((0, 1, 3, {"label": "also important"}) == e for e in edges)
|
||||
@@ -0,0 +1,131 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph.classes import operation
|
||||
from easygraph.utils import edges_equal
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"graph_type", [eg.Graph, eg.DiGraph, eg.MultiGraph, eg.MultiDiGraph]
|
||||
)
|
||||
def test_selfloops(graph_type):
|
||||
G = eg.complete_graph(3, create_using=graph_type)
|
||||
G.add_edge(0, 0)
|
||||
assert edges_equal(eg.selfloop_edges(G), [(0, 0)])
|
||||
assert edges_equal(eg.selfloop_edges(G, data=True), [(0, 0, {})])
|
||||
assert eg.number_of_selfloops(G) == 1
|
||||
|
||||
|
||||
def test_set_edge_attributes_scalar():
|
||||
G = eg.path_graph(3)
|
||||
eg.set_edge_attributes(G, 5, "weight")
|
||||
for _, _, data in G.edges:
|
||||
assert data["weight"] == 5
|
||||
|
||||
|
||||
def test_set_edge_attributes_dict():
|
||||
G = eg.path_graph(3)
|
||||
attrs = {(0, 1): 3, (1, 2): 7}
|
||||
eg.set_edge_attributes(G, attrs, "weight")
|
||||
assert G[0][1]["weight"] == 3
|
||||
assert G[1][2]["weight"] == 7
|
||||
|
||||
|
||||
def test_set_edge_attributes_dict_of_dict():
|
||||
G = eg.path_graph(3)
|
||||
attrs = {(0, 1): {"a": 1}, (1, 2): {"b": 2}}
|
||||
eg.set_edge_attributes(G, attrs)
|
||||
assert G[0][1]["a"] == 1
|
||||
assert G[1][2]["b"] == 2
|
||||
|
||||
|
||||
def test_set_node_attributes_scalar():
|
||||
G = eg.path_graph(3)
|
||||
eg.set_node_attributes(G, 42, "level")
|
||||
for n in G.nodes:
|
||||
assert G.nodes[n]["level"] == 42
|
||||
|
||||
|
||||
def test_set_node_attributes_dict():
|
||||
G = eg.path_graph(3)
|
||||
eg.set_node_attributes(G, {0: "x", 1: "y"}, name="tag")
|
||||
assert G.nodes[0]["tag"] == "x"
|
||||
assert G.nodes[1]["tag"] == "y"
|
||||
|
||||
|
||||
def test_set_node_attributes_dict_of_dict():
|
||||
G = eg.path_graph(3)
|
||||
eg.set_node_attributes(G, {0: {"foo": 10}, 1: {"bar": 20}})
|
||||
assert G.nodes[0]["foo"] == 10
|
||||
assert G.nodes[1]["bar"] == 20
|
||||
|
||||
|
||||
def test_add_path_structure_and_attrs():
|
||||
G = eg.Graph()
|
||||
eg.add_path(G, [10, 11, 12], weight=9)
|
||||
actual_edges = {(u, v) for u, v, _ in G.edges}
|
||||
assert actual_edges == {(10, 11), (11, 12)}
|
||||
assert G[10][11]["weight"] == 9
|
||||
assert G[11][12]["weight"] == 9
|
||||
|
||||
|
||||
def test_topological_sort_linear():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
assert list(operation.topological_sort(G)) == [1, 2, 3]
|
||||
|
||||
|
||||
def test_topological_sort_cycle():
|
||||
G = eg.DiGraph([(0, 1), (1, 2), (2, 0)])
|
||||
with pytest.raises(AssertionError, match="contains a cycle"):
|
||||
list(operation.topological_sort(G))
|
||||
|
||||
|
||||
def test_selfloop_edges_variants():
|
||||
G = eg.MultiGraph()
|
||||
G.add_edge(0, 0, key="x", label="loop")
|
||||
G.add_edge(1, 1, key="y", label="loop2")
|
||||
basic = list(eg.selfloop_edges(G))
|
||||
with_data = list(eg.selfloop_edges(G, data=True))
|
||||
with_keys = list(eg.selfloop_edges(G, keys=True))
|
||||
full = list(eg.selfloop_edges(G, keys=True, data="label"))
|
||||
assert (0, 0) in basic and (1, 1) in basic
|
||||
assert all(len(t) == 3 for t in with_data)
|
||||
assert all(len(t) == 3 for t in with_keys)
|
||||
assert "x" in [k for _, _, k, _ in full]
|
||||
|
||||
|
||||
def test_number_of_selfloops():
|
||||
G = eg.MultiGraph()
|
||||
G.add_edges_from([(0, 0), (1, 1), (1, 2)])
|
||||
assert eg.number_of_selfloops(G) == 2
|
||||
|
||||
|
||||
def test_density_undirected():
|
||||
G = eg.complete_graph(5)
|
||||
d = eg.density(G)
|
||||
assert pytest.approx(d, 0.01) == 1.0
|
||||
|
||||
|
||||
def test_density_directed():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 2)])
|
||||
d = eg.density(G)
|
||||
assert pytest.approx(d, 0.01) == 2 / (3 * (3 - 1)) # 2/6
|
||||
|
||||
|
||||
def test_topological_generations_linear():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (2, 3), (3, 4)])
|
||||
generations = list(operation.topological_generations(G))
|
||||
assert generations == [[1], [2], [3], [4]]
|
||||
|
||||
|
||||
def test_topological_generations_branching():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (1, 3), (2, 4), (3, 4)])
|
||||
generations = list(operation.topological_generations(G))
|
||||
# Valid topological generations: [1], [2, 3], [4]
|
||||
assert generations[0] == [1]
|
||||
assert set(generations[1]) == {2, 3}
|
||||
assert generations[2] == [4]
|
||||
Reference in New Issue
Block a user