diff --git a/docs/.buildinfo b/docs/.buildinfo index e79de496..e20fee0d 100644 --- a/docs/.buildinfo +++ b/docs/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 7192bb01cb11925cfee048363011ddbc +config: 46d551dfdf92abdfae075ddecdb73176 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/_images/res_clusterings.png b/docs/_images/res_clusterings.png new file mode 100644 index 00000000..94c1deb3 Binary files /dev/null and b/docs/_images/res_clusterings.png differ diff --git a/docs/_images/res_constraint.png b/docs/_images/res_constraint.png new file mode 100644 index 00000000..bc504ad7 Binary files /dev/null and b/docs/_images/res_constraint.png differ diff --git a/docs/_modules/easygraph/classes/base.html b/docs/_modules/easygraph/classes/base.html deleted file mode 100644 index 86495894..00000000 --- a/docs/_modules/easygraph/classes/base.html +++ /dev/null @@ -1,1015 +0,0 @@ - - - - - - easygraph.classes.base — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.classes.base

-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
-
-
-__all__ = ["load_structure", "BaseHypergraph"]
-
-
-
[docs]def load_structure(file_path: Union[str, Path]): - r"""Load a DHG's structure from a file. The supported structure includes: ``Graph``, ``DiGraph``, ``BiGraph``, ``Hypergraph``. - - Args: - ``file_path`` (``Union[str, Path]``): The file path to load the DHG'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
- - -
[docs]class BaseHypergraph: - r"""The ``BaseHypergraph`` class is the base class for all hypergraph structures. - - Args: - ``num_v`` (``int``): The number of vertices in the hypergraph. - ``e_list_v2e`` (``Union[List[int], List[List[int]]]``, optional): A list of hyperedges describes how the vertices point to the hyperedges. Defaults to ``None``. - ``e_list_e2v`` (``Union[List[int], List[List[int]]]``, optional): A list of hyperedges describes how the hyperedges point to the vertices. Defaults to ``None``. - ``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``. - ``v_weight`` (``Union[float, List[float]]``, optional): Weights for vertices. If set to ``None``, the value ``1`` is used for all vertices. Defaults to ``None``. - ``device`` (``torch.device``, optional): The device to store the hypergraph. Defaults to ``torch.device('cpu')``. - """ - - def __init__( - self, - num_v: int, - e_list_v2e: Optional[Union[List[int], List[List[int]]]] = None, - e_list_e2v: Optional[Union[List[int], List[List[int]]]] = None, - 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, - v_weight: Optional[List[float]] = None, - device: torch.device = torch.device("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 = device - - @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.""" - -
[docs] @abc.abstractmethod - def save(self, file_path: Union[str, Path]): - r"""Save the DHG's hypergraph structure to a file. - - Args: - ``file_path`` (``str``): The file_path to store the DHG's hypergraph structure. - """
- -
[docs] @staticmethod - @abc.abstractmethod - def load(file_path: Union[str, Path]): - r"""Load the DHG's hypergraph structure from a file. - - Args: - ``file_path`` (``str``): The file path to load the DHG's hypergraph structure. - """
- -
[docs] @staticmethod - @abc.abstractmethod - def from_state_dict(state_dict: dict): - r"""Load the DHG's hypergraph structure from the state dict. - - Args: - ``state_dict`` (``dict``): The state dict to load the DHG's hypergraph. - """
- -
[docs] @abc.abstractmethod - def draw(self, **kwargs): - r"""Draw the structure."""
- -
[docs] 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): - self.cache = {} - if group_name is None: - self.group_cache = defaultdict(dict) - else: - self.group_cache.pop(group_name, None) - -
[docs] @abc.abstractmethod - def clone(self) -> "BaseHypergraph": - r"""Return a copy of this type of hypergraph."""
- -
[docs] def to(self, device: torch.device): - r"""Move the hypergraph to the specified device. - - Args: - ``device`` (``torch.device``): The device to store the hypergraph. - """ - self.device = 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 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(e_list[_idx])) - return e_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_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 = [content["w_e"] for content in self._raw_groups[group_name].values()] - W = torch.tensor(w_list, device=self.device).view((-1, 1)) - return W - - # some structure modification functions -
[docs] 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.group_names: - 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 - ) - -
[docs] 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)
- -
[docs] @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``. - """
- -
[docs] @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.""" - -
[docs] @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 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 - -
[docs] 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"] - -
[docs] 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"] - -
[docs] 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"] - -
[docs] 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"] - -
[docs] 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"] - -
[docs] 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 -
[docs] 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 -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
- -
[docs] @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``. - """
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/classes/directed_graph.html b/docs/_modules/easygraph/classes/directed_graph.html deleted file mode 100644 index fc13ca9c..00000000 --- a/docs/_modules/easygraph/classes/directed_graph.html +++ /dev/null @@ -1,1330 +0,0 @@ - - - - - - easygraph.classes.directed_graph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.classes.directed_graph

-from typing import Dict
-from typing import List
-
-import easygraph.convert as convert
-
-from easygraph.classes.graph import Graph
-from easygraph.utils.exception import EasyGraphError
-
-
-
[docs]class DiGraph(Graph): - """ - Base class for directed graphs. - - Nodes are allowed for any hashable Python objects, including int, string, dict, etc. - Edges are stored as Python dict type, with optional key/value attributes. - - Parameters - ---------- - graph_attr : keywords arguments, optional (default : None) - Attributes to add to graph as key=value pairs. - - See Also - -------- - Graph - - Examples - -------- - Create an empty directed graph with no nodes and edges. - - >>> G = eg.Graph() - - Create a deep copy graph *G2* from existing Graph *G1*. - - >>> G2 = G1.copy() - - Create an graph with attributes. - - >>> G = eg.Graph(name='Karate Club', date='2020.08.21') - - **Attributes:** - - Returns the adjacency matrix of the graph. - - >>> G.adj - - Returns all the nodes with their attributes. - - >>> G.nodes - - Returns all the edges with their attributes. - - >>> G.edges - - """ - - gnn_data_dict_factory = dict - graph_attr_dict_factory = dict - node_dict_factory = dict - node_attr_dict_factory = dict - adjlist_outer_dict_factory = dict - adjlist_inner_dict_factory = dict - edge_attr_dict_factory = dict - - def __init__(self, incoming_graph_data=None, **graph_attr): - self.graph = self.graph_attr_dict_factory() - self._ndata = self.gnn_data_dict_factory() - self._node = self.node_dict_factory() - self._adj = self.adjlist_outer_dict_factory() - self._pred = self.adjlist_outer_dict_factory() - self.cflag = 0 - if incoming_graph_data is not None: - convert.to_easygraph_graph(incoming_graph_data, create_using=self) - self.graph.update(graph_attr) - - def __iter__(self): - return iter(self._node) - - def __len__(self): - return len(self._node) - - def __contains__(self, node): - try: - return node in self._node - except TypeError: - return False - - def __getitem__(self, node): - # return list(self._adj[node].keys()) - return self._adj[node] - - @property - def ndata(self): - return self._ndata - - @property - def pred(self): - return self._pred - - @property - def adj(self): - return self._adj - - @property - def nodes(self): - return self._node - # return [node for node in self._node] - - @property - def edges(self): - edges = list() - for u in self._adj: - for v in self._adj[u]: - edges.append((u, v, self._adj[u][v])) - return edges - - @property - def name(self): - """String identifier of the graph. - - This graph attribute appears in the attribute dict G.graph - keyed by the string `"name"`. as well as an attribute (technically - a property) `G.name`. This is entirely user controlled. - """ - return self.graph.get("name", "") - - @name.setter - def name(self, s): - self.graph["name"] = s - -
[docs] def out_degree(self, weight="weight"): - """Returns the weighted out degree of each node. - - Parameters - ---------- - weight : string, optional (default : 'weight') - Weight key of the original weighted graph. - - Returns - ------- - out_degree : dict - Each node's (key) weighted out degree (value). - - Notes - ----- - If the graph is not weighted, all the weights will be regarded as 1. - - See Also - -------- - in_degree - degree - - Examples - -------- - - >>> G.out_degree(weight='weight') - - """ - degree = dict() - for u, v, d in self.edges: - if u in degree: - degree[u] += d.get(weight, 1) - else: - degree[u] = d.get(weight, 1) - - # For isolated nodes - for node in self.nodes: - if node not in degree: - degree[node] = 0 - - return degree
- -
[docs] def in_degree(self, weight="weight"): - """Returns the weighted in degree of each node. - - Parameters - ---------- - weight : string, optional (default : 'weight') - Weight key of the original weighted graph. - - Returns - ------- - in_degree : dict - Each node's (key) weighted in degree (value). - - Notes - ----- - If the graph is not weighted, all the weights will be regarded as 1. - - See Also - -------- - out_degree - degree - - Examples - -------- - - >>> G.in_degree(weight='weight') - - """ - degree = dict() - for u, v, d in self.edges: - if v in degree: - degree[v] += d.get(weight, 1) - else: - degree[v] = d.get(weight, 1) - - # For isolated nodes - for node in self.nodes: - if node not in degree: - degree[node] = 0 - - return degree
- -
[docs] def degree(self, weight="weight"): - """Returns the weighted degree of each node, i.e. sum of out/in degree. - - Parameters - ---------- - weight : string, optional (default : 'weight') - Weight key of the original weighted graph. - - Returns - ------- - degree : dict - Each node's (key) weighted in degree (value). - For directed graph, it returns the sum of out degree and in degree. - - Notes - ----- - If the graph is not weighted, all the weights will be regarded as 1. - - See also - -------- - out_degree - in_degree - - Examples - -------- - - >>> G.degree() - >>> G.degree(weight='weight') - - or you can customize the weight key - - >>> G.degree(weight='weight_1') - - """ - degree = dict() - outdegree = self.out_degree(weight=weight) - indegree = self.in_degree(weight=weight) - for u in outdegree: - degree[u] = outdegree[u] + indegree[u] - return degree
- -
[docs] def size(self, weight=None): - """Returns the number of edges or total of all edge weights. - - Parameters - ----------- - weight : String or None, optional - The weight key. If None, it will calculate the number of - edges, instead of total of all edge weights. - - Returns - ------- - size : int or float, optional (default: None) - The number of edges or total of all edge weights. - - Examples - -------- - - Returns the number of edges in G: - - >>> G.size() - - Returns the total of all edge weights in G: - - >>> G.size(weight='weight') - - """ - s = sum(d for v, d in self.out_degree(weight=weight).items()) - return int(s) if weight is None else s
- -
[docs] def number_of_edges(self, u=None, v=None): - """Returns the number of edges between two nodes. - - Parameters - ---------- - u, v : nodes, optional (default=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 graphs, this method counts the total number of - edges in the graph: - - >>> G = eg.path_graph(4) - >>> 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) - 1 - - For directed graphs, this method can count the total number of - directed edges from `u` to `v`: - - >>> G = eg.DiGraph() - >>> G.add_edge(0, 1) - >>> G.add_edge(1, 0) - >>> G.number_of_edges(0, 1) - 1 - - """ - if u is None: - return int(self.size()) - if v in self._adj[u]: - return 1 - return 0
- -
[docs] def nbunch_iter(self, nbunch=None): - """Returns an iterator over nodes contained in nbunch that are - also in the graph. - - The nodes in nbunch are checked for membership in the graph - and if not are silently ignored. - - Parameters - ---------- - nbunch : single node, container, or all nodes (default= all nodes) - The view will only report edges incident to these nodes. - - Returns - ------- - niter : iterator - An iterator over nodes in nbunch that are also in the graph. - If nbunch is None, iterate over all nodes in the graph. - - Raises - ------ - EasyGraphError - If nbunch is not a node or sequence of nodes. - If a node in nbunch is not hashable. - - See Also - -------- - Graph.__iter__ - - Notes - ----- - When nbunch is an iterator, the returned iterator yields values - directly from nbunch, becoming exhausted when nbunch is exhausted. - - To test whether nbunch is a single node, one can use - "if nbunch in self:", even after processing with this routine. - - If nbunch is not a node or a (possibly empty) sequence/iterator - or None, a :exc:`EasyGraphError` is raised. Also, if any object in - nbunch is not hashable, a :exc:`EasyGraphError` is raised. - """ - if nbunch is None: # include all nodes via iterator - bunch = iter(self._adj) - elif nbunch in self: # if nbunch is a single node - bunch = iter([nbunch]) - else: # if nbunch is a sequence of nodes - - def bunch_iter(nlist, adj): - try: - for n in nlist: - if n in adj: - yield n - except TypeError as err: - exc, message = err, err.args[0] - # capture error for non-sequence/iterator nbunch. - if "iter" in message: - exc = EasyGraphError( - "nbunch is not a node or a sequence of nodes." - ) - # capture error for unhashable node. - if "hashable" in message: - exc = EasyGraphError( - f"Node {n} in sequence nbunch is not a valid node." - ) - raise exc - - bunch = bunch_iter(nbunch, self._adj) - return bunch
- -
[docs] def neighbors(self, node): - """Returns an iterator of a node's neighbors (successors). - - Parameters - ---------- - node : Hashable - The target node. - - Returns - ------- - neighbors : iterator - An iterator of a node's neighbors (successors). - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_edges([(1,2), (2,3), (2,4)]) - >>> for neighbor in G.neighbors(node=2): - ... print(neighbor) - - """ - # successors - try: - return iter(self._adj[node]) - except KeyError: - print("No node {}".format(node))
- - successors = neighbors - -
[docs] def predecessors(self, node): - """Returns an iterator of a node's neighbors (predecessors). - - Parameters - ---------- - node : Hashable - The target node. - - Returns - ------- - neighbors : iterator - An iterator of a node's neighbors (predecessors). - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_edges([(1,2), (2,3), (2,4)]) - >>> for predecessor in G.predecessors(node=2): - ... print(predecessor) - - """ - # predecessors - try: - return iter(self._pred[node]) - except KeyError: - print("No node {}".format(node))
- -
[docs] def all_neighbors(self, node): - """Returns an iterator of a node's neighbors, including both successors and predecessors. - - Parameters - ---------- - node : Hashable - The target node. - - Returns - ------- - neighbors : iterator - An iterator of a node's neighbors, including both successors and predecessors. - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_edges([(1,2), (2,3), (2,4)]) - >>> for neighbor in G.all_neighbors(node=2): - ... print(neighbor) - - """ - # union of successors and predecessors - try: - neighbors = list(self._adj[node]) - neighbors.extend(self._pred[node]) - return iter(neighbors) - except KeyError: - print("No node {}".format(node))
- -
[docs] def add_node(self, node_for_adding, **node_attr): - """Add one node - - Add one node, type of which is any hashable Python object, such as int, string, dict, or even Graph itself. - You can add with node attributes using Python dict type. - - Parameters - ---------- - node_for_adding : any hashable Python object - Nodes for adding. - - node_attr : keywords arguments, optional - The node attributes. - You can customize them with different key-value pairs. - - See Also - -------- - add_nodes - - Examples - -------- - >>> G.add_node('a') - >>> G.add_node('hello world') - >>> G.add_node('Jack', age=10) - - >>> G.add_node('Jack', **{ - ... 'age': 10, - ... 'gender': 'M' - ... }) - - """ - self._add_one_node(node_for_adding, node_attr)
- -
[docs] def add_nodes(self, nodes_for_adding: list, nodes_attr: List[Dict] = []): - """Add nodes with a list of nodes. - - Parameters - ---------- - nodes_for_adding : list - - nodes_attr : list of dict - The corresponding attribute for each of *nodes_for_adding*. - - See Also - -------- - add_node - - Examples - -------- - Add nodes with a list of nodes. - You can add with node attributes using a list of Python dict type, - each of which is the attribute of each node, respectively. - - >>> G.add_nodes([1, 2, 'a', 'b']) - >>> G.add_nodes(range(1, 200)) - - >>> G.add_nodes(['Jack', 'Tom', 'Lily'], nodes_attr=[ - ... { - ... 'age': 10, - ... 'gender': 'M' - ... }, - ... { - ... 'age': 11, - ... 'gender': 'M' - ... }, - ... { - ... 'age': 10, - ... 'gender': 'F' - ... } - ... ]) - - """ - if nodes_attr is None: - nodes_attr = [] - if not len(nodes_attr) == 0: # Nodes attributes included in input - assert len(nodes_for_adding) == len( - nodes_attr - ), "Nodes and Attributes lists must have same length." - else: # Set empty attribute for each node - nodes_attr = [dict() for i in range(len(nodes_for_adding))] - - for i in range(len(nodes_for_adding)): - try: - self._add_one_node(nodes_for_adding[i], nodes_attr[i]) - except Exception as err: - print(err) - pass
- -
[docs] def add_nodes_from(self, nodes_for_adding, **attr): - """Add multiple nodes. - - Parameters - ---------- - nodes_for_adding : iterable container - A container of nodes (list, dict, set, etc.). - OR - A container of (node, attribute dict) tuples. - Node attributes are updated using the attribute dict. - attr : keyword arguments, optional (default= no attributes) - Update attributes for all nodes in nodes. - Node attributes specified in nodes as a tuple take - precedence over attributes specified via keyword arguments. - - See Also - -------- - add_node - - Examples - -------- - >>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc - >>> G.add_nodes_from("Hello") - >>> K3 = eg.Graph([(0, 1), (1, 2), (2, 0)]) - >>> G.add_nodes_from(K3) - >>> sorted(G.nodes(), key=str) - [0, 1, 2, 'H', 'e', 'l', 'o'] - - Use keywords to update specific node attributes for every node. - - >>> G.add_nodes_from([1, 2], size=10) - >>> G.add_nodes_from([3, 4], weight=0.4) - - Use (node, attrdict) tuples to update attributes for specific nodes. - - >>> G.add_nodes_from([(1, dict(size=11)), (2, {"color": "blue"})]) - >>> G.nodes[1]["size"] - 11 - >>> H = eg.Graph() - >>> H.add_nodes_from(G.nodes(data=True)) - >>> H.nodes[1]["size"] - 11 - - """ - for n in nodes_for_adding: - try: - newnode = n not in self._node - newdict = attr - except TypeError: - n, ndict = n - newnode = n not in self._node - newdict = attr.copy() - newdict.update(ndict) - if newnode: - if n is None: - raise ValueError("None cannot be a node") - self._adj[n] = self.adjlist_inner_dict_factory() - self._pred[n] = self.adjlist_inner_dict_factory() - self._node[n] = self.node_attr_dict_factory() - self._node[n].update(newdict)
- - def _add_one_node(self, one_node_for_adding, node_attr: dict = {}): - node = one_node_for_adding - if node not in self._node: - self._adj[node] = self.adjlist_inner_dict_factory() - self._pred[node] = self.adjlist_inner_dict_factory() - - attr_dict = self._node[node] = self.node_attr_dict_factory() - attr_dict.update(node_attr) - else: # If already exists, there is no complain and still updating the node attribute - self._node[node].update(node_attr) - -
[docs] def add_edge(self, u_of_edge, v_of_edge, **edge_attr): - """Add a directed edge. - - Parameters - ---------- - u_of_edge : object - The start end of this edge - - v_of_edge : object - The destination end of this edge - - edge_attr : keywords arguments, optional - The attribute of the edge. - - Notes - ----- - Nodes of this edge will be automatically added to the graph, if they do not exist. - - See Also - -------- - add_edges - - Examples - -------- - - >>> G.add_edge(1,2) - >>> G.add_edge('Jack', 'Tom', weight=10) - - Add edge with attributes, edge weight, for example, - - >>> G.add_edge(1, 2, **{ - ... 'weight': 20 - ... }) - - """ - self._add_one_edge(u_of_edge, v_of_edge, edge_attr)
- -
[docs] def add_weighted_edge(self, u_of_edge, v_of_edge, weight): - self._add_one_edge(u_of_edge, v_of_edge, edge_attr={"weight": weight})
- -
[docs] def add_edges(self, edges_for_adding, edges_attr: List[Dict] = []): - """Add a list of edges. - - Parameters - ---------- - edges_for_adding : list of 2-element tuple - The edges for adding. Each element is a (u, v) tuple, and u, v are - start end and destination end, respectively. - - edges_attr : list of dict, optional - The corresponding attributes for each edge in *edges_for_adding*. - - Examples - -------- - Add a list of edges into *G* - - >>> G.add_edges([ - ... (1, 2), - ... (3, 4), - ... ('Jack', 'Tom') - ... ]) - - Add edge with attributes, for example, edge weight, - - >>> G.add_edges([(1,2), (2, 3)], edges_attr=[ - ... { - ... 'weight': 20 - ... }, - ... { - ... 'weight': 15 - ... } - ... ]) - - """ - if edges_attr is None: - edges_attr = [] - if not len(edges_attr) == 0: # Edges attributes included in input - assert len(edges_for_adding) == len( - edges_attr - ), "Edges and Attributes lists must have same length." - else: # Set empty attribute for each edge - edges_attr = [dict() for i in range(len(edges_for_adding))] - - for i in range(len(edges_for_adding)): - try: - edge = edges_for_adding[i] - attr = edges_attr[i] - assert len(edge) == 2, "Edge tuple {} must be 2-tuple.".format(edge) - self._add_one_edge(edge[0], edge[1], attr) - except Exception as err: - print(err)
- -
[docs] 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 must be given as 2-tuples (u, v) or - 3-tuples (u, v, d) where d is a dictionary containing edge data. - attr : keyword arguments, optional - Edge data (or labels or objects) can be assigned using - keyword arguments. - - 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. - - 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") - """ - for e in ebunch_to_add: - ne = len(e) - if ne == 3: - u, v, dd = e - elif ne == 2: - u, v = e - dd = {} - else: - raise EasyGraphError(f"Edge tuple {e} must be a 2-tuple or 3-tuple.") - 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() - datadict = self._adj[u].get(v, self.edge_attr_dict_factory()) - datadict.update(attr) - datadict.update(dd) - self._adj[u][v] = datadict - self._pred[v][u] = datadict
- -
[docs] def add_edges_from_file(self, file, weighted=False): - """Added edges from file - For example, txt files, - - Each line is in form like: - a b 23.0 - which denotes an edge `a → b` with weight 23.0. - - Parameters - ---------- - file : string - The file path. - - weighted : boolean, optional (default : False) - If the file consists of weight information, set `True`. - The weight key will be set as 'weight'. - - Examples - -------- - - If `./club_network.txt` is: - - Jack Mary 23.0 - - Mary Tom 15.0 - - Tom Ben 20.0 - - Then add them to *G* - - >>> G.add_edges_from_file(file='./club_network.txt', weighted=True) - - - """ - import re - - with open(file, "r") as fp: - edges = fp.readlines() - if weighted: - for edge in edges: - edge = re.sub(",", " ", edge) - edge = edge.split() - try: - self.add_edge(edge[0], edge[1], weight=float(edge[2])) - except: - pass - else: - for edge in edges: - edge = re.sub(",", " ", edge) - edge = edge.split() - try: - self.add_edge(edge[0], edge[1]) - except: - pass
- - def _add_one_edge(self, u_of_edge, v_of_edge, edge_attr: dict = {}): - u, v = u_of_edge, v_of_edge - # add nodes - if u not in self._node: - self._add_one_node(u) - if v not in self._node: - self._add_one_node(v) - # add the edge - datadict = self._adj[u].get(v, self.edge_attr_dict_factory()) - datadict.update(edge_attr) - self._adj[u][v] = datadict - self._pred[v][u] = datadict - -
[docs] def remove_node(self, node_to_remove): - """Remove one node from your graph. - - Parameters - ---------- - node_to_remove : object - The node you want to remove. - - See Also - -------- - remove_nodes - - Examples - -------- - Remove node *Jack* from *G* - - >>> G.remove_node('Jack') - - """ - try: - succs = list(self._adj[node_to_remove]) - preds = list(self._pred[node_to_remove]) - del self._node[node_to_remove] - except KeyError: # Node not exists in self - raise KeyError("No node {} in graph.".format(node_to_remove)) - for succ in succs: # Remove edges start with node_to_remove - del self._pred[succ][node_to_remove] - for pred in preds: # Remove edges end with node_to_remove - del self._adj[pred][node_to_remove] - - # Remove this node - del self._adj[node_to_remove] - del self._pred[node_to_remove]
- -
[docs] def remove_nodes(self, nodes_to_remove: list): - """Remove nodes from your graph. - - Parameters - ---------- - nodes_to_remove : list of object - The list of nodes you want to remove. - - See Also - -------- - remove_node - - Examples - -------- - Remove node *[1, 2, 'a', 'b']* from *G* - - >>> G.remove_nodes([1, 2, 'a', 'b']) - - """ - for ( - node - ) in ( - nodes_to_remove - ): # If not all nodes included in graph, give up removing other nodes - assert node in self._node, "Remove Error: No node {} in graph".format(node) - for node in nodes_to_remove: - self.remove_node(node)
- -
[docs] def remove_edge(self, u, v): - """Remove one edge from your graph. - - Parameters - ---------- - u : object - The start end of the edge. - - v : object - The destination end of the edge. - - See Also - -------- - remove_edges - - Examples - -------- - Remove edge (1,2) from *G* - - >>> G.remove_edge(1,2) - - """ - try: - del self._adj[u][v] - del self._pred[v][u] - except KeyError: - raise KeyError("No edge {}-{} in graph.".format(u, v))
- -
[docs] def remove_edges(self, edges_to_remove: [tuple]): - """Remove a list of edges from your graph. - - Parameters - ---------- - edges_to_remove : list of tuple - The list of edges you want to remove, - Each element is (u, v) tuple, which denote the start and destination - end of the edge, respectively. - - See Also - -------- - remove_edge - - Examples - -------- - Remove the edges *('Jack', 'Mary')* amd *('Mary', 'Tom')* from *G* - - >>> G.remove_edge([ - ... ('Jack', 'Mary'), - ... ('Mary', 'Tom') - ... ]) - - """ - for edge in edges_to_remove: - u, v = edge[:2] - self.remove_edge(u, v)
- -
[docs] 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) edge between u and v. - - 3-tuples (u, v, k) where k 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 - -------- - >>> G = eg.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc - >>> ebunch = [(1, 2), (2, 3)] - >>> G.remove_edges_from(ebunch) - """ - for e in ebunch: - u, v = e[:2] # ignore edge data - if u in self._adj and v in self._adj[u]: - del self._adj[u][v] - del self._pred[v][u]
- -
[docs] def has_node(self, node): - return node in self._node
- -
[docs] def has_edge(self, u, v): - try: - return v in self._adj[u] - except KeyError: - return False
- -
[docs] def number_of_nodes(self): - """Returns the number of nodes. - - Returns - ------- - number_of_nodes : int - The number of nodes. - """ - return len(self._node)
- -
[docs] def is_directed(self): - return True
- -
[docs] def is_multigraph(self): - """Returns True if graph is a multigraph, False otherwise.""" - return False
- -
[docs] def copy(self): - """Return a deep copy of the graph. - - Returns - ------- - copy : easygraph.DiGraph - A deep copy of the original graph. - - Examples - -------- - *G2* is a deep copy of *G1* - - >>> G2 = G1.copy() - - """ - G = self.__class__() - G.graph.update(self.graph) - for node, node_attr in self._node.items(): - G.add_node(node, **node_attr) - for u, nbrs in self._adj.items(): - for v, edge_data in nbrs.items(): - G.add_edge(u, v, **edge_data) - - return G
- -
[docs] def nodes_subgraph(self, from_nodes: list): - """Returns a subgraph of some nodes - - Parameters - ---------- - from_nodes : list of object - The nodes in subgraph. - - Returns - ------- - nodes_subgraph : easygraph.Graph - The subgraph consisting of *from_nodes*. - - Examples - -------- - - >>> G = eg.Graph() - >>> G.add_edges([(1,2), (2,3), (2,4), (4,5)]) - >>> G_sub = G.nodes_subgraph(from_nodes= [1,2,3]) - - """ - # Edge - from_nodes = set(from_nodes) - G = self.__class__() - G.graph.update(self.graph) - from_nodes = set(from_nodes) - for node in from_nodes: - try: - G.add_node(node, **self._node[node]) - except KeyError: - pass - - for v, edge_data in self._adj[node].items(): - if v in from_nodes: - G.add_edge(node, v, **edge_data) - return G
- -
[docs] def ego_subgraph(self, center): - """Returns an ego network graph of a node. - - Parameters - ---------- - center : object - The center node of the ego network graph - - Returns - ------- - ego_subgraph : easygraph.Graph - The ego network graph of *center*. - - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_edges([ - ... ('Jack', 'Maria'), - ... ('Maria', 'Andy'), - ... ('Jack', 'Tom') - ... ]) - >>> G.ego_subgraph(center='Jack') - """ - neighbors_of_center = list(self.all_neighbors(center)) - neighbors_of_center.append(center) - return self.nodes_subgraph(from_nodes=neighbors_of_center)
- -
[docs] def to_index_node_graph(self, begin_index=0): - """Returns a deep copy of graph, with each node switched to its index. - - Considering that the nodes of your graph may be any possible hashable Python object, - you can get an isomorphic graph of the original one, with each node switched to its index. - - Parameters - ---------- - begin_index : int - The begin index of the index graph. - - Returns - ------- - G : easygraph.Graph - Deep copy of graph, with each node switched to its index. - - index_of_node : dict - Index of node - - node_of_index : dict - Node of index - - Examples - -------- - The following method returns this isomorphic graph and index-to-node dictionary - as well as node-to-index dictionary. - - >>> G = eg.Graph() - >>> G.add_edges([ - ... ('Jack', 'Maria'), - ... ('Maria', 'Andy'), - ... ('Jack', 'Tom') - ... ]) - >>> G_index_graph, index_of_node, node_of_index = G.to_index_node_graph() - - """ - G = self.__class__() - G.graph.update(self.graph) - index_of_node = dict() - node_of_index = dict() - for index, (node, node_attr) in enumerate(self._node.items()): - G.add_node(index + begin_index, **node_attr) - index_of_node[node] = index + begin_index - node_of_index[index + begin_index] = node - for u, nbrs in self._adj.items(): - for v, edge_data in nbrs.items(): - G.add_edge(index_of_node[u], index_of_node[v], **edge_data) - - return G, index_of_node, node_of_index
- -
[docs] def cpp(self): - G = DiGraphC() - G.graph.update(self.graph) - for u, attr in self.nodes.items(): - G.add_node(u, **attr) - for u, v, attr in self.edges: - G.add_edge(u, v, **attr) - G.generate_linkgraph() - return G
- - -try: - import cpp_easygraph - - class DiGraphC(cpp_easygraph.DiGraph): - cflag = 1 - -except ImportError: - -
[docs] class DiGraphC: - def __init__(self, **graph_attr): - print( - "Object cannot be instantiated because C extension has not been" - " successfully compiled and installed. Please refer to" - " https://github.com/easy-graph/Easy-Graph/blob/master/README.rst and" - " reinstall easygraph." - ) - raise RuntimeError
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/classes/directed_multigraph.html b/docs/_modules/easygraph/classes/directed_multigraph.html deleted file mode 100644 index 6061c232..00000000 --- a/docs/_modules/easygraph/classes/directed_multigraph.html +++ /dev/null @@ -1,530 +0,0 @@ - - - - - - easygraph.classes.directed_multigraph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.classes.directed_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.directed_graph import DiGraph
-from easygraph.classes.multigraph import MultiGraph
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = ["MultiDiGraph"]
-
-
-
[docs]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) - -
[docs] 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 - # 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
- -
[docs] 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._nodes: - 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._nodes: - 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._nodes: - preds = self._pred[n] - deg = sum(len(keys) for keys in preds.values()) - degree[n] = deg - else: - for n in self._nodes: - 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._nodes: - succs = self._adj[n] - deg = sum(len(keys) for keys in succs.values()) - degree[n] = deg - else: - for n in self._nodes: - 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 - -
[docs] def is_multigraph(self): - """Returns True if graph is a multigraph, False otherwise.""" - return True
- -
[docs] def is_directed(self): - """Returns True if graph is directed, False otherwise.""" - return True
- -
[docs] 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
- -
[docs] 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(keys=True, data=True) - ) - return H - return eg.graphviews.reverse_view(self)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/classes/graph.html b/docs/_modules/easygraph/classes/graph.html deleted file mode 100644 index 206dbc22..00000000 --- a/docs/_modules/easygraph/classes/graph.html +++ /dev/null @@ -1,1637 +0,0 @@ - - - - - - easygraph.classes.graph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.classes.graph

-import warnings
-
-from copy import deepcopy
-from typing import Dict
-from typing import List
-from typing import Tuple
-
-import easygraph as eg
-import easygraph.convert as convert
-
-from easygraph.utils.exception import EasyGraphError
-from easygraph.utils.exception import EasyGraphException
-from easygraph.utils.sparse import sparse_dropout
-
-
-
[docs]class Graph: - """ - Base class for undirected graphs. - - Nodes are allowed for any hashable Python objects, including int, string, dict, etc. - Edges are stored as Python dict type, with optional key/value attributes. - - Parameters - ---------- - graph_attr : keywords arguments, optional (default : None) - Attributes to add to graph as key=value pairs. - - See Also - -------- - DiGraph - - Examples - -------- - Create an empty undirected graph with no nodes and edges. - - >>> G = eg.Graph() - - Create a deep copy graph *G2* from existing Graph *G1*. - - >>> G2 = G1.copy() - - Create an graph with attributes. - - >>> G = eg.Graph(name='Karate Club', date='2020.08.21') - - **Attributes:** - - Returns the adjacency matrix of the graph. - - >>> G.adj - - Returns all the nodes with their attributes. - - >>> G.nodes - - Returns all the edges with their attributes. - - >>> G.edges - - """ - - gnn_data_dict_factory = dict - raw_selfloop_dict = dict - graph_attr_dict_factory = dict - node_dict_factory = dict - node_attr_dict_factory = dict - adjlist_outer_dict_factory = dict - adjlist_inner_dict_factory = dict - edge_attr_dict_factory = dict - node_index_dict = dict - - def __init__(self, incoming_graph_data=None, extra_selfloop=False, **graph_attr): - self.graph = self.graph_attr_dict_factory() - self._node = self.node_dict_factory() - self._adj = self.adjlist_outer_dict_factory() - self._ndata = self.gnn_data_dict_factory() - self._raw_selfloop_dict = self.raw_selfloop_dict() - self.extra_selfloop = extra_selfloop - self.cache = {} - self._node_index = self.node_index_dict() - self.cflag = 0 - self._id = 0 - self.device = "cpu" - if incoming_graph_data is not None: - convert.to_easygraph_graph(incoming_graph_data, create_using=self) - self.graph.update(graph_attr) - - def __iter__(self): - return iter(self._node) - - def __len__(self): - return len(self._node) - - def __contains__(self, node): - try: - return node in self._node - except TypeError: - return False - - def __getitem__(self, node): - # return list(self._adj[node].keys()) - return self._adj[node] - - @property - def ndata(self): - return self._ndata - - @property - def adj(self): - return self._adj - - @property - def nodes(self): - return self._node - # return [node for node in self._node] - - @property - def node_index(self): - return self._node_index - - @property - def node_index(self): - return self._node_index - - @property - def edges(self): - if self.cache.get("edges") != None: - return self.cache["edges"] - edge_lst = list() - seen = set() - for u in self._adj: - for v in self._adj[u]: - if (u, v) not in seen: - seen.add((u, v)) - seen.add((v, u)) - edge_lst.append((u, v, self._adj[u][v])) - del seen - self.cache["edge"] = edge_lst - return self.cache["edge"] - - @property - def name(self): - """String identifier of the graph. - - This graph attribute appears in the attribute dict G.graph - keyed by the string `"name"`. as well as an attribute (technically - a property) `G.name`. This is entirely user controlled. - """ - return self.graph.get("name", "") - - @property - def e_both_side(self, weight="weight") -> Tuple[List[List], List[float]]: - if self.cache.get("e_both_side") != None: - return self.cache["e_both_side"] - edges = list() - weights = list() - seen = set() - for u in self._adj: - for v in self._adj[u]: - if (u, v) not in seen: - seen.add((u, v)) - seen.add((v, u)) - edges.append([u, v]) - edges.append([v, u]) - if weight not in self._adj[u][v]: - warnings.warn("There is no property %s,default to 1" % (weight)) - weights.append(1.0) - weights.append(1.0) - else: - if type(self._adj[u][v][weight]) == float: - weights.append(self._adj[u][v][weight]) - weights.append(self._adj[u][v][weight]) - else: - raise EasyGraphException("The type of weight must be float") - del seen - self.cache["e_both_side"] = (edges, weights) - return self.cache["e_both_side"] - -
[docs] @staticmethod - def from_hypergraph_hypergcn( - hypergraph, - feature, - with_mediator=False, - remove_selfloop=True, - ): - import torch - - r"""Construct a graph from a hypergraph with methods proposed in `HyperGCN: A New Method of Training Graph Convolutional Networks on Hypergraphs <https://arxiv.org/pdf/1809.02589.pdf>`_ paper . - - Args: - ``hypergraph`` (``Hypergraph``): The source hypergraph. - ``feature`` (``torch.Tensor``): The feature of the vertices. - ``with_mediator`` (``str``): Whether to use mediator to transform the hyperedges to edges in the graph. Defaults to ``False``. - ``remove_selfloop`` (``bool``): Whether to remove self-loop. Defaults to ``True``. - ``device`` (``torch.device``): The device to store the graph. Defaults to ``torch.device("cpu")``. - """ - num_v = hypergraph.num_v - assert ( - num_v == feature.shape[0] - ), "The number of vertices in hypergraph and feature.shape[0] must be equal!" - e_list, new_e_list, new_e_weight = hypergraph.e[0], [], [] - rv = torch.rand((feature.shape[1], 1), device=feature.device) - for e in e_list: - num_v_in_e = len(e) - assert ( - num_v_in_e >= 2 - ), "The number of vertices in an edge must be greater than or equal to 2!" - p = torch.mm(feature[e, :], rv).squeeze() - v_a_idx, v_b_idx = torch.argmax(p), torch.argmin(p) - if not with_mediator: - new_e_list.append([e[v_a_idx], e[v_b_idx]]) - new_e_weight.append(1.0 / num_v_in_e) - else: - w = 1.0 / (2 * num_v_in_e - 3) - for mid_v_idx in range(num_v_in_e): - if mid_v_idx != v_a_idx and mid_v_idx != v_b_idx: - new_e_list.append([e[v_a_idx], e[mid_v_idx]]) - new_e_weight.append(w) - new_e_list.append([e[v_b_idx], e[mid_v_idx]]) - new_e_weight.append(w) - # remove selfloop - if remove_selfloop: - new_e_list = torch.tensor(new_e_list, dtype=torch.long) - new_e_weight = torch.tensor(new_e_weight, dtype=torch.float) - e_mask = (new_e_list[:, 0] != new_e_list[:, 1]).bool() - new_e_list = new_e_list[e_mask].numpy().tolist() - new_e_weight = new_e_weight[e_mask].numpy().tolist() - - _g = Graph() - _g.add_nodes(list(range(0, num_v))) - for ( - e, - w, - ) in zip(new_e_list, new_e_weight): - if _g.has_edge(e[0], e[1]): - _g.add_edge(e[0], e[1], weight=(w + _g.adj[e[0]][e[1]]["weight"])) - else: - _g.add_edge(e[0], e[1], weight=w) - return _g
- - @property - def A(self): - import torch - - r"""Return the adjacency matrix :math:`\mathbf{A}` of the sample graph with ``torch.sparse_coo_tensor`` format. Size :math:`(|\mathcal{V}|, |\mathcal{V}|)`. - """ - if self.cache.get("A", None) is None: - if len(self.edges) == 0: - self.cache["A"] = torch.sparse_coo_tensor( - size=(len(self.nodes), len(self.edges)), device=self.device - ) - else: - e_list, e_weight = self.e_both_side - self.cache["A"] = torch.sparse_coo_tensor( - indices=torch.tensor(e_list).t(), - values=torch.tensor(e_weight), - size=(len(self.nodes), len(self.nodes)), - device=self.device, - ).coalesce() - return self.cache["A"] - - @property - def D_v_neg_1_2( - self, - ): - import torch - - r"""Return the nomalized diagnal matrix of vertex degree :math:`\mathbf{D}_v^{-\frac{1}{2}}` with ``torch.sparse_coo_tensor`` format. Size :math:`(|\mathcal{V}|, |\mathcal{V}|)`. - """ - if self.cache.get("D_v_neg_1_2") is None: - _mat = self.D_v.clone() - _val = _mat._values() ** -0.5 - _val[torch.isinf(_val)] = 0 - self.cache["D_v_neg_1_2"] = torch.sparse_coo_tensor( - _mat._indices(), _val, _mat.size(), device=self.device - ).coalesce() - return self.cache["D_v_neg_1_2"] - - @property - def node2index(self): - if self.cache.get("node2index", None) is None: - node2index_dict = {} - index = 0 - for n in self.nodes: - node2index_dict[n] = index - index += 1 - self.cache["node2index"] = node2index_dict - return self.cache["node2index"] - - @property - def e(self) -> Tuple[List[List[int]], List[float]]: - r"""Return the edge list and weight list in the graph.""" - - if self.cache.get("e", None) is None: - node2index = self.node2index - e_list = [ - (node2index[src_idx], node2index[dst_idx]) - for src_idx, dst_idx, d in self.edges - ] - w_list = [] - for d in self.edges: - if "weight" not in d[2]: - w_list.append(1.0) - else: - w_list.append(d[2]["weight"]) - # e_list.extend([(v_idx, v_idx) for v_idx in self._raw_selfloop_dict.keys()]) - # w_list.extend(list(self._raw_selfloop_dict.values())) - # if self._has_extra_selfloop: - # e_list.extend((v_idx, v_idx) for v_idx in range(self.num_v)) - # w_list.extend([1.0] * self.num_v) - self.cache["e"] = e_list, w_list - return self.cache["e"] - - @property - def D_v(self): - import torch - - r"""Return the diagnal matrix of vertex degree :math:`\mathbf{D}_v` with ``torch.sparse_coo_tensor`` format. Size :math:`(|\mathcal{V}|, |\mathcal{V}|)`. - """ - if self.cache.get("D_v") is None: - _tmp = torch.sparse.sum(self.A, dim=1).to_dense().clone().view(-1) - self.cache["D_v"] = torch.sparse_coo_tensor( - torch.arange(0, len(self.nodes)).view(1, -1).repeat(2, 1), - _tmp, - torch.Size([len(self.nodes), len(self.nodes)]), - device=self.device, - ).coalesce() - return self.cache["D_v"] - -
[docs] def add_extra_selfloop(self): - r"""Add extra selfloops to the graph.""" - self._has_extra_selfloop = True - self._clear_cache()
- -
[docs] def remove_extra_selfloop(self): - r"""Remove extra selfloops from the graph.""" - self._has_extra_selfloop = False - self._clear_cache()
- -
[docs] def remove_selfloop(self): - r"""Remove all selfloops from the graph.""" - self._raw_selfloop_dict.clear() - self.remove_extra_selfloop() - self._clear_cache()
- -
[docs] def nbr_v(self, v_idx: int) -> Tuple[List[int], List[float]]: - r"""Return a vertex list of the neighbors of the vertex ``v_idx``. - - Args: - ``v_idx`` (``int``): The index of the vertex. - """ - return self.N_v(v_idx).cpu().numpy().tolist()
- -
[docs] def N_v(self, v_idx: int) -> Tuple[List[int], List[float]]: - r"""Return the neighbors of the vertex ``v_idx`` with ``torch.Tensor`` format. - - Args: - ``v_idx`` (``int``): The index of the vertex. - """ - sub_v_set = self.A[v_idx]._indices()[0].clone() - return sub_v_set
- -
[docs] def clone(self): - r"""Clone the graph.""" - # _g = Graph(self.num_v, extra_selfloop=self._has_extra_selfloop, device=self.device) - # _g=self.__class__() - # _g.device="cpu" - # _g.extra_selfloop=False - # _g.edges = deepcopy(self.edges) - # _g.cache = deepcopy(self.cache) - return self.copy()
- - @name.setter - def name(self, s): - self.graph["name"] = s - -
[docs] def degree(self, weight="weight"): - """Returns the weighted degree of of each node. - - Parameters - ---------- - weight : string, optional (default: 'weight') - Weight key of the original weighted graph. - - Returns - ------- - degree : dict - Each node's (key) weighted degree (value). - - Notes - ----- - If the graph is not weighted, all the weights will be regarded as 1. - - Examples - -------- - You can call with no attributes, if 'weight' is the weight key: - - >>> G.degree() - - if you have customized weight key 'weight_1'. - - >>> G.degree(weight='weight_1') - - """ - if self.cache.get("degree") != None: - return self.cache["degree"] - degree = dict() - for u, v, d in self.edges: - if u in degree: - degree[u] += d.get(weight, 1) - else: - degree[u] = d.get(weight, 1) - if v in degree: - degree[v] += d.get(weight, 1) - else: - degree[v] = d.get(weight, 1) - - # For isolated nodes - for node in self.nodes: - if node not in degree: - degree[node] = 0 - self.cache["degree"] = degree - return degree
- -
[docs] def order(self): - """Returns the number of nodes in the graph. - - Returns - ------- - nnodes : int - The number of nodes in the graph. - - See Also - -------- - number_of_nodes: identical method - __len__: identical method - - Examples - -------- - >>> G = eg.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc - >>> G.order() - 3 - """ - return len(self._node)
- -
[docs] def size(self, weight=None): - """Returns the number of edges or total of all edge weights. - - Parameters - ----------- - weight : String or None, optional - The weight key. If None, it will calculate the number of - edges, instead of total of all edge weights. - - Returns - ------- - size : int or float, optional (default: None) - The number of edges or total of all edge weights. - - Examples - -------- - - Returns the number of edges in G: - - >>> G.size() - - Returns the total of all edge weights in G: - - >>> G.size(weight='weight') - - """ - if self.cache.get("size") != None: - return self.cache["size"] - s = sum(d for v, d in self.degree(weight=weight).items()) - self.cache["size"] = s // 2 if weight is None else s / 2 - return self.cache["size"]
- - # GCN Laplacian smoothing - @property - def L_GCN(self): - r"""Return the GCN Laplacian matrix :math:`\mathcal{L}_{GCN}` of the graph with ``torch.sparse_coo_tensor`` format. Size :math:`(|\mathcal{V}|, |\mathcal{V}|)`. - - .. math:: - \mathcal{L}_{GCN} = \mathbf{\hat{D}}_v^{-\frac{1}{2}} \mathbf{\hat{A}} \mathbf{\hat{D}}_v^{-\frac{1}{2}} - - """ - if self.cache.get("L_GCN") is None: - _tmp_g = self.clone() - _tmp_g.add_extra_selfloop() - self.cache["L_GCN"] = ( - _tmp_g.D_v_neg_1_2.mm(_tmp_g.A) - .mm(_tmp_g.D_v_neg_1_2) - .clone() - .coalesce() - ) - return self.cache["L_GCN"] - -
[docs] def smoothing_with_GCN(self, X, drop_rate=0.0): - r"""Return the smoothed feature matrix with GCN Laplacian matrix :math:`\mathcal{L}_{GCN}`. - - Args: - ``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in adjacency matrix with probability ``drop_rate``. Default: ``0.0``. - """ - if drop_rate > 0.0: - L_GCN = sparse_dropout(self.L_GCN, drop_rate) - else: - L_GCN = self.L_GCN - return L_GCN.mm(X)
- -
[docs] def number_of_edges(self, u=None, v=None): - """Returns the number of edges between two nodes. - - Parameters - ---------- - u, v : nodes, optional (default=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 graphs, this method counts the total number of - edges in the graph: - - >>> G = eg.path_graph(4) - >>> 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) - 1 - - For directed graphs, this method can count the total number of - directed edges from `u` to `v`: - - >>> G = eg.DiGraph() - >>> G.add_edge(0, 1) - >>> G.add_edge(1, 0) - >>> G.number_of_edges(0, 1) - 1 - - """ - if u is None: - return int(self.size()) - if v in self._adj[u]: - return 1 - return 0
- -
[docs] def nbunch_iter(self, nbunch=None): - """Returns an iterator over nodes contained in nbunch that are - also in the graph. - - The nodes in nbunch are checked for membership in the graph - and if not are silently ignored. - - Parameters - ---------- - nbunch : single node, container, or all nodes (default= all nodes) - The view will only report edges incident to these nodes. - - Returns - ------- - niter : iterator - An iterator over nodes in nbunch that are also in the graph. - If nbunch is None, iterate over all nodes in the graph. - - Raises - ------ - EasyGraphError - If nbunch is not a node or sequence of nodes. - If a node in nbunch is not hashable. - - See Also - -------- - Graph.__iter__ - - Notes - ----- - When nbunch is an iterator, the returned iterator yields values - directly from nbunch, becoming exhausted when nbunch is exhausted. - - To test whether nbunch is a single node, one can use - "if nbunch in self:", even after processing with this routine. - - If nbunch is not a node or a (possibly empty) sequence/iterator - or None, a :exc:`EasyGraphError` is raised. Also, if any object in - nbunch is not hashable, a :exc:`EasyGraphError` is raised. - """ - if nbunch is None: # include all nodes via iterator - bunch = iter(self._adj) - elif nbunch in self: # if nbunch is a single node - bunch = iter([nbunch]) - else: # if nbunch is a sequence of nodes - - def bunch_iter(nlist, adj): - try: - for n in nlist: - if n in adj: - yield n - except TypeError as err: - exc, message = err, err.args[0] - # capture error for non-sequence/iterator nbunch. - if "iter" in message: - exc = EasyGraphError( - "nbunch is not a node or a sequence of nodes." - ) - # capture error for unhashable node. - if "hashable" in message: - exc = EasyGraphError( - f"Node {n} in sequence nbunch is not a valid node." - ) - raise exc - - bunch = bunch_iter(nbunch, self._adj) - return bunch
- -
[docs] def neighbors(self, node): - """Returns an iterator of a node's neighbors. - - Parameters - ---------- - node : Hashable - The target node. - - Returns - ------- - neighbors : iterator - An iterator of a node's neighbors. - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_edges([(1,2), (2,3), (2,4)]) - >>> for neighbor in G.neighbors(node=2): - ... print(neighbor) - - """ - try: - return iter(self._adj[node]) - except KeyError: - print("No node {}".format(node))
- - all_neighbors = neighbors - -
[docs] def add_node(self, node_for_adding, **node_attr): - """Add one node - - Add one node, type of which is any hashable Python object, such as int, string, dict, or even Graph itself. - You can add with node attributes using Python dict type. - - Parameters - ---------- - node_for_adding : any hashable Python object - Nodes for adding. - - node_attr : keywords arguments, optional - The node attributes. - You can customize them with different key-value pairs. - - See Also - -------- - add_nodes - - Examples - -------- - >>> G.add_node('a') - >>> G.add_node('hello world') - >>> G.add_node('Jack', age=10) - - >>> G.add_node('Jack', **{ - ... 'age': 10, - ... 'gender': 'M' - ... }) - - """ - self._add_one_node(node_for_adding, node_attr) - self._clear_cache()
- -
[docs] def add_nodes(self, nodes_for_adding: list, nodes_attr: List[Dict] = []): - """Add nodes with a list of nodes. - - Parameters - ---------- - nodes_for_adding : list - - nodes_attr : list of dict - The corresponding attribute for each of *nodes_for_adding*. - - See Also - -------- - add_node - - Examples - -------- - Add nodes with a list of nodes. - You can add with node attributes using a list of Python dict type, - each of which is the attribute of each node, respectively. - - >>> G.add_nodes([1, 2, 'a', 'b']) - >>> G.add_nodes(range(1, 200)) - - >>> G.add_nodes(['Jack', 'Tom', 'Lily'], nodes_attr=[ - ... { - ... 'age': 10, - ... 'gender': 'M' - ... }, - ... { - ... 'age': 11, - ... 'gender': 'M' - ... }, - ... { - ... 'age': 10, - ... 'gender': 'F' - ... } - ... ]) - - """ - if not len(nodes_attr) == 0: # Nodes attributes included in input - assert len(nodes_for_adding) == len( - nodes_attr - ), "Nodes and Attributes lists must have same length." - else: # Set empty attribute for each node - nodes_attr = [dict() for i in range(len(nodes_for_adding))] - - for i in range(len(nodes_for_adding)): - try: - self._add_one_node(nodes_for_adding[i], nodes_attr[i]) - except Exception as err: - print(err) - pass - self._clear_cache()
- -
[docs] def add_nodes_from(self, nodes_for_adding, **attr): - """Add multiple nodes. - - Parameters - ---------- - nodes_for_adding : iterable container - A container of nodes (list, dict, set, etc.). - OR - A container of (node, attribute dict) tuples. - Node attributes are updated using the attribute dict. - attr : keyword arguments, optional (default= no attributes) - Update attributes for all nodes in nodes. - Node attributes specified in nodes as a tuple take - precedence over attributes specified via keyword arguments. - - See Also - -------- - add_node - - Examples - -------- - >>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc - >>> G.add_nodes_from("Hello") - >>> K3 = eg.Graph([(0, 1), (1, 2), (2, 0)]) - >>> G.add_nodes_from(K3) - >>> sorted(G.nodes(), key=str) - [0, 1, 2, 'H', 'e', 'l', 'o'] - - Use keywords to update specific node attributes for every node. - - >>> G.add_nodes_from([1, 2], size=10) - >>> G.add_nodes_from([3, 4], weight=0.4) - - Use (node, attrdict) tuples to update attributes for specific nodes. - - >>> G.add_nodes_from([(1, dict(size=11)), (2, {"color": "blue"})]) - >>> G.nodes[1]["size"] - 11 - >>> H = eg.Graph() - >>> H.add_nodes_from(G.nodes(data=True)) - >>> H.nodes[1]["size"] - 11 - - """ - for n in nodes_for_adding: - try: - newnode = n not in self._node - newdict = attr - except TypeError: - n, ndict = n - newnode = n not in self._node - newdict = attr.copy() - newdict.update(ndict) - if newnode: - if n is None: - raise ValueError("None cannot be a node") - self._adj[n] = self.adjlist_inner_dict_factory() - self._node[n] = self.node_attr_dict_factory() - self._node[n].update(newdict) - self._clear_cache()
- - def _add_one_node(self, one_node_for_adding, node_attr: dict = {}): - node = one_node_for_adding - if node not in self._node: - self._node_index[node] = self._id - self._id += 1 - self._adj[node] = self.adjlist_inner_dict_factory() - attr_dict = self._node[node] = self.node_attr_dict_factory() - attr_dict.update(node_attr) - else: # If already exists, there is no complain and still updating the node attribute - self._node[node].update(node_attr) - self._clear_cache() - -
[docs] def add_edge(self, u_of_edge, v_of_edge, **edge_attr): - """Add one edge. - - Parameters - ---------- - u_of_edge : object - One end of this edge - - v_of_edge : object - The other one end of this edge - - edge_attr : keywords arguments, optional - The attribute of the edge. - - Notes - ----- - Nodes of this edge will be automatically added to the graph, if they do not exist. - - See Also - -------- - add_edges - - Examples - -------- - - >>> G.add_edge(1,2) - >>> G.add_edge('Jack', 'Tom', weight=10) - - Add edge with attributes, edge weight, for example, - - >>> G.add_edge(1, 2, **{ - ... 'weight': 20 - ... }) - - """ - self._add_one_edge(u_of_edge, v_of_edge, edge_attr) - self._clear_cache()
- -
[docs] def add_weighted_edge(self, u_of_edge, v_of_edge, weight): - self._add_one_edge(u_of_edge, v_of_edge, edge_attr={"weight": weight}) - self._clear_cache()
- -
[docs] def add_edges(self, edges_for_adding, edges_attr: List[Dict] = []): - """Add a list of edges. - - Parameters - ---------- - edges_for_adding : list of 2-element tuple - The edges for adding. Each element is a (u, v) tuple, and u, v are - two ends of the edge. - - edges_attr : list of dict, optional - The corresponding attributes for each edge in *edges_for_adding*. - - Examples - -------- - Add a list of edges into *G* - - >>> G.add_edges([ - ... (1, 2), - ... (3, 4), - ... ('Jack', 'Tom') - ... ]) - - Add edge with attributes, for example, edge weight, - - >>> G.add_edges([(1,2), (2, 3)], edges_attr=[ - ... { - ... 'weight': 20 - ... }, - ... { - ... 'weight': 15 - ... } - ... ]) - - """ - if edges_attr is None: - edges_attr = [] - if not len(edges_attr) == 0: # Edges attributes included in input - assert len(edges_for_adding) == len( - edges_attr - ), "Edges and Attributes lists must have same length." - else: # Set empty attribute for each edge - edges_attr = [dict() for i in range(len(edges_for_adding))] - - for i in range(len(edges_for_adding)): - try: - edge = edges_for_adding[i] - attr = edges_attr[i] - assert len(edge) == 2, "Edge tuple {} must be 2-tuple.".format(edge) - self._add_one_edge(edge[0], edge[1], attr) - except Exception as err: - print(err) - self._clear_cache()
- -
[docs] 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 must be given as 2-tuples (u, v) or - 3-tuples (u, v, d) where d is a dictionary containing edge data. - attr : keyword arguments, optional - Edge data (or labels or objects) can be assigned using - keyword arguments. - - 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. - - 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") - """ - for e in ebunch_to_add: - ne = len(e) - if ne == 3: - u, v, dd = e - elif ne == 2: - u, v = e - dd = {} # doesn't need edge_attr_dict_factory - else: - raise EasyGraphError(f"Edge tuple {e} must be a 2-tuple or 3-tuple.") - if u not in self._node: - 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._node: - 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() - datadict = self._adj[u].get(v, self.edge_attr_dict_factory()) - datadict.update(attr) - datadict.update(dd) - self._adj[u][v] = datadict - self._adj[v][u] = datadict - self._clear_cache()
- - def add_weighted_edges_from(self, ebunch_to_add, weight="weight", **attr): - """Add weighted edges in `ebunch_to_add` with specified weight attr - - Parameters - ---------- - ebunch_to_add : container of edges - Each edge given in the list or container will be added - to the graph. The edges must be given as 3-tuples (u, v, w) - where w is a number. - weight : string, optional (default= 'weight') - The attribute name for the edge weights to be added. - attr : keyword arguments, optional (default= no attributes) - Edge attributes to add/update for all edges. - - See Also - -------- - add_edge : add a single edge - add_edges_from : add multiple edges - - Notes - ----- - Adding the same edge twice for Graph/DiGraph simply updates - the edge data. For MultiGraph/MultiDiGraph, duplicate edges - are stored. - - Examples - -------- - >>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc - >>> G.add_weighted_edges_from([(0, 1, 3.0), (1, 2, 7.5)]) - """ - self.add_edges_from(((u, v, {weight: d}) for u, v, d in ebunch_to_add), **attr) - -
[docs] def add_weighted_edges_from(self, ebunch_to_add, weight="weight", **attr): - """Add weighted edges in `ebunch_to_add` with specified weight attr - - Parameters - ---------- - ebunch_to_add : container of edges - Each edge given in the list or container will be added - to the graph. The edges must be given as 3-tuples (u, v, w) - where w is a number. - weight : string, optional (default= 'weight') - The attribute name for the edge weights to be added. - attr : keyword arguments, optional (default= no attributes) - Edge attributes to add/update for all edges. - - See Also - -------- - add_edge : add a single edge - add_edges_from : add multiple edges - - Notes - ----- - Adding the same edge twice for Graph/DiGraph simply updates - the edge data. For MultiGraph/MultiDiGraph, duplicate edges - are stored. - - Examples - -------- - >>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc - >>> G.add_weighted_edges_from([(0, 1, 3.0), (1, 2, 7.5)]) - """ - self.add_edges_from(((u, v, {weight: d}) for u, v, d in ebunch_to_add), **attr)
- -
[docs] def add_edges_from_file(self, file, weighted=False): - """Added edges from file - For example, txt files, - - Each line is in form like: - a b 23.0 - which denotes an edge (a, b) with weight 23.0. - - Parameters - ---------- - file : string - The file path. - - weighted : boolean, optional (default : False) - If the file consists of weight information, set `True`. - The weight key will be set as 'weight'. - - Examples - -------- - - If `./club_network.txt` is: - - Jack Mary 23.0 - - Mary Tom 15.0 - - Tom Ben 20.0 - - Then add them to *G* - - >>> G.add_edges_from_file(file='./club_network.txt', weighted=True) - - - """ - import re - - with open(file, "r") as fp: - edges = fp.readlines() - if weighted: - for edge in edges: - edge = re.sub(",", " ", edge) - edge = edge.split() - try: - self.add_edge(edge[0], edge[1], weight=float(edge[2])) - except: - pass - else: - for edge in edges: - edge = re.sub(",", " ", edge) - edge = edge.split() - try: - self.add_edge(edge[0], edge[1]) - except: - pass
- -
[docs] def remove_nodes_from(self, nodes): - """Remove multiple nodes. - - Parameters - ---------- - nodes : iterable container - A container of nodes (list, dict, set, etc.). If a node - in the container is not in the graph it is silently - ignored. - - See Also - -------- - remove_node - - Examples - -------- - >>> G = eg.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc - >>> e = list(G.nodes) - >>> e - [0, 1, 2] - >>> G.remove_nodes_from(e) - >>> list(G.nodes) - [] - - """ - adj = self._adj - for n in nodes: - try: - del self._node[n] - for u in list(adj[n]): # list handles self-loops - del adj[u][n] # (allows mutation of dict in loop) - del adj[n] - except KeyError: - pass
- - def _add_one_edge(self, u_of_edge, v_of_edge, edge_attr: dict = {}): - u, v = u_of_edge, v_of_edge - # add nodes - if u not in self._node: - self._add_one_node(u) - if v not in self._node: - self._add_one_node(v) - # add the edge - datadict = self._adj[u].get(v, self.edge_attr_dict_factory()) - datadict.update(edge_attr) - self._adj[u][v] = datadict - self._adj[v][u] = datadict - if u == v: - self.extra_selfloop = True - self._raw_selfloop_dict[u] = datadict - self._clear_cache() - -
[docs] def remove_node(self, node_to_remove): - """Remove one node from your graph. - - Parameters - ---------- - node_to_remove : object - The node you want to remove. - - See Also - -------- - remove_nodes - - Examples - -------- - Remove node *Jack* from *G* - - >>> G.remove_node('Jack') - - """ - try: - neighbors = list(self._adj[node_to_remove]) - del self._node[node_to_remove] - except KeyError: # Node not exists in self - raise EasyGraphError("No node {} in graph.".format(node_to_remove)) - for neighbor in neighbors: # Remove edges with other nodes - del self._adj[neighbor][node_to_remove] - del self._adj[node_to_remove] # Remove this node - self._clear_cache()
- -
[docs] def remove_nodes(self, nodes_to_remove: list): - """Remove nodes from your graph. - - Parameters - ---------- - nodes_to_remove : list of object - The list of nodes you want to remove. - - See Also - -------- - remove_node - - Examples - -------- - Remove node *[1, 2, 'a', 'b']* from *G* - - >>> G.remove_nodes([1, 2, 'a', 'b']) - - """ - for ( - node - ) in ( - nodes_to_remove - ): # If not all nodes included in graph, give up removing other nodes - assert node in self._node, "Remove Error: No node {} in graph".format(node) - for node in nodes_to_remove: - self.remove_node(node) - self._clear_cache()
- -
[docs] def remove_edge(self, u, v): - """Remove one edge from your graph. - - Parameters - ---------- - u : object - One end of the edge. - - v : object - The other end of the edge. - - See Also - -------- - remove_edges - - Examples - -------- - Remove edge (1,2) from *G* - - >>> G.remove_edge(1,2) - - """ - try: - del self._adj[u][v] - if u != v: # self-loop needs only one entry removed - del self._adj[v][u] - - self._clear_cache() - except KeyError: - raise KeyError("No edge {}-{} in graph.".format(u, v))
- -
[docs] def remove_edges(self, edges_to_remove: [tuple]): - """Remove a list of edges from your graph. - - Parameters - ---------- - edges_to_remove : list of tuple - The list of edges you want to remove, - Each element is (u, v) tuple, which denote the two ends of the edge. - - See Also - -------- - remove_edge - - Examples - -------- - Remove the edges *('Jack', 'Mary')* amd *('Mary', 'Tom')* from *G* - - >>> G.remove_edge([ - ... ('Jack', 'Mary'), - ... ('Mary', 'Tom') - ... ]) - - """ - for edge in edges_to_remove: - u, v = edge[:2] - self.remove_edge(u, v) - self._clear_cache()
- -
[docs] def has_node(self, node): - return node in self._node
- -
[docs] def has_edge(self, u, v): - try: - return v in self._adj[u] - except KeyError: - return False
- -
[docs] def number_of_nodes(self): - """Returns the number of nodes. - - Returns - ------- - number_of_nodes : int - The number of nodes. - """ - return len(self._node)
- -
[docs] def is_directed(self): - return False
- -
[docs] def is_multigraph(self): - """Returns True if graph is a multigraph, False otherwise.""" - return False
- -
[docs] def copy(self): - """Return a deep copy of the graph. - - Returns - ------- - copy : easygraph.Graph - A deep copy of the original graph. - - Examples - -------- - *G2* is a deep copy of *G1* - - >>> G2 = G1.copy() - - """ - G = self.__class__() - G.graph.update(self.graph) - for node, node_attr in self._node.items(): - G.add_node(node, **node_attr) - for u, nbrs in self._adj.items(): - for v, edge_data in nbrs.items(): - G.add_edge(u, v, **edge_data) - - return G
- -
[docs] def nodes_subgraph(self, from_nodes: list): - """Returns a subgraph of some nodes - - Parameters - ---------- - from_nodes : list of object - The nodes in subgraph. - - Returns - ------- - nodes_subgraph : easygraph.Graph - The subgraph consisting of *from_nodes*. - - Examples - -------- - - >>> G = eg.Graph() - >>> G.add_edges([(1,2), (2,3), (2,4), (4,5)]) - >>> G_sub = G.nodes_subgraph(from_nodes= [1,2,3]) - - """ - G = self.__class__() - G.graph.update(self.graph) - from_nodes = set(from_nodes) - for node in from_nodes: - try: - G.add_node(node, **self._node[node]) - except KeyError: - pass - - for v, edge_data in self._adj[node].items(): - if v in from_nodes: - G.add_edge(node, v, **edge_data) - return G
- -
[docs] def ego_subgraph(self, center): - """Returns an ego network graph of a node. - - Parameters - ---------- - center : object - The center node of the ego network graph - - Returns - ------- - ego_subgraph : easygraph.Graph - The ego network graph of *center*. - - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_edges([ - ... ('Jack', 'Maria'), - ... ('Maria', 'Andy'), - ... ('Jack', 'Tom') - ... ]) - >>> G.ego_subgraph(center='Jack') - """ - neighbors_of_center = list(self.all_neighbors(center)) - neighbors_of_center.append(center) - return self.nodes_subgraph(from_nodes=neighbors_of_center)
- -
[docs] def to_index_node_graph(self, begin_index=0): - """Returns a deep copy of graph, with each node switched to its index. - - Considering that the nodes of your graph may be any possible hashable Python object, - you can get an isomorphic graph of the original one, with each node switched to its index. - - Parameters - ---------- - begin_index : int - The begin index of the index graph. - - Returns - ------- - G : easygraph.Graph - Deep copy of graph, with each node switched to its index. - - index_of_node : dict - Index of node - - node_of_index : dict - Node of index - - Examples - -------- - The following method returns this isomorphic graph and index-to-node dictionary - as well as node-to-index dictionary. - - >>> G = eg.Graph() - >>> G.add_edges([ - ... ('Jack', 'Maria'), - ... ('Maria', 'Andy'), - ... ('Jack', 'Tom') - ... ]) - >>> G_index_graph, index_of_node, node_of_index = G.to_index_node_graph() - - """ - G = self.__class__() - G.graph.update(self.graph) - index_of_node = dict() - node_of_index = dict() - for index, (node, node_attr) in enumerate(self._node.items()): - G.add_node(index + begin_index, **node_attr) - index_of_node[node] = index + begin_index - node_of_index[index + begin_index] = node - for u, nbrs in self._adj.items(): - for v, edge_data in nbrs.items(): - G.add_edge(index_of_node[u], index_of_node[v], **edge_data) - - return G, index_of_node, node_of_index
- -
[docs] def to_directed_class(self): - """Returns the class to use for empty directed copies. - - If you subclass the base classes, use this to designate - what directed class to use for `to_directed()` copies. - """ - return eg.DiGraph
- -
[docs] def to_directed(self): - """Returns a directed representation of the graph. - - Returns - ------- - G : DiGraph - 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 Graph to use dict-like objects - in the data structure, those changes do not transfer to the - DiGraph 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)] - """ - graph_class = self.to_directed_class() - - G = graph_class() - 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, deepcopy(data)) - for u, nbrs in self._adj.items() - for v, data in nbrs.items() - ) - return G
- - def _clear_cache(self): - r"""Clear the cache.""" - self.cache = {} - -
[docs] def cpp(self): - G = GraphC() - G.graph.update(self.graph) - for u, attr in self.nodes.items(): - G.add_node(u, **attr) - for u, v, attr in self.edges: - G.add_edge(u, v, **attr) - G.generate_linkgraph() - return G
- - -try: - import cpp_easygraph - - class GraphC(cpp_easygraph.Graph): - cflag = 1 - -except ImportError: - -
[docs] class GraphC: - def __init__(self, **graph_attr): - print( - "Object cannot be instantiated because C extension has not been" - " successfully compiled and installed. Please refer to" - " https://github.com/easy-graph/Easy-Graph/blob/master/README.rst and" - " reinstall easygraph." - ) - raise RuntimeError
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/classes/graphviews.html b/docs/_modules/easygraph/classes/graphviews.html deleted file mode 100644 index ca75583c..00000000 --- a/docs/_modules/easygraph/classes/graphviews.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - easygraph.classes.graphviews — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.classes.graphviews

-from easygraph.utils import only_implemented_for_Directed_graph
-
-
-__all__ = ["reverse_view"]
-
-
-
[docs]@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
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/classes/hypergraph.html b/docs/_modules/easygraph/classes/hypergraph.html deleted file mode 100644 index b6df9b88..00000000 --- a/docs/_modules/easygraph/classes/hypergraph.html +++ /dev/null @@ -1,1905 +0,0 @@ - - - - - - easygraph.classes.hypergraph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.classes.hypergraph

-import pickle
-import random
-
-from copy import deepcopy
-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 torch
-
-from easygraph.classes.base import BaseHypergraph
-from easygraph.functions.drawing.drawing import draw_hypergraph
-from easygraph.utils.sparse import sparse_dropout
-
-
-__all__ = ["Hypergraph"]
-
-
-
[docs]class Hypergraph(BaseHypergraph): - r"""The ``Hypergraph`` class is developed for hypergraph structures. - - Args: - ``num_v`` (``int``): The number of vertices in the hypergraph. - ``e_list`` (``Union[List[int], List[List[int]]]``, optional): A list of hyperedges describes how the vertices point to the hyperedges. 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 operation to merge those conflicting hyperedges in the same hyperedge group, which can be ``'mean'``, ``'sum'`` or ``'max'``. Defaults to ``'mean'``. - ``device`` (``torch.device``, optional): The device to store the hypergraph. Defaults to ``torch.device('cpu')``. - """ - - def __init__( - self, - num_v: int, - e_list: Optional[Union[List[int], List[List[int]]]] = None, - e_weight: Optional[Union[float, List[float]]] = None, - merge_op: str = "mean", - device: torch.device = torch.device("cpu"), - ): - super().__init__(num_v, device=device) - if e_list is not None: - self.add_hyperedges(e_list, e_weight, merge_op=merge_op) - - def __repr__(self) -> str: - r"""Print the hypergraph information.""" - return f"Hypergraph(num_vertex={self.num_v}, num_hyperedge={self.num_e})" - - @property - def state_dict(self) -> Dict[str, Any]: - r"""Get the state dict of the hypergraph.""" - return {"num_v": self.num_v, "raw_groups": self._raw_groups} - -
[docs] def save(self, file_path: Union[str, Path]): - r"""Save the DHG's hypergraph structure a file. - - Args: - ``file_path`` (``Union[str, Path]``): The file path to store the DHG's hypergraph structure. - """ - file_path = Path(file_path) - assert file_path.parent.exists(), "The directory does not exist." - data = { - "class": "Hypergraph", - "state_dict": self.state_dict, - } - with open(file_path, "wb") as fp: - pickle.dump(data, fp)
- -
[docs] @staticmethod - def load(file_path: Union[str, Path]): - r"""Load the DHG's hypergraph structure from a file. - - Args: - ``file_path`` (``Union[str, Path]``): The file path to load the DHG's hypergraph structure. - """ - file_path = Path(file_path) - assert file_path.exists(), "The file does not exist." - with open(file_path, "rb") as fp: - data = pickle.load(fp) - assert data["class"] == "Hypergraph", "The file is not a DHG's hypergraph file." - return Hypergraph.from_state_dict(data["state_dict"])
- -
[docs] def draw( - self, - e_style: str = "circle", - v_label: Optional[List[str]] = None, - v_size: Union[float, list] = 1.0, - v_color: Union[str, list] = "r", - v_line_width: Union[str, list] = 1.0, - e_color: Union[str, list] = "gray", - e_fill_color: Union[str, list] = "whitesmoke", - e_line_width: Union[str, list] = 1.0, - font_size: float = 1.0, - font_family: str = "sans-serif", - push_v_strength: float = 1.0, - push_e_strength: float = 1.0, - pull_e_strength: float = 1.0, - pull_center_strength: float = 1.0, - ): - r"""Draw the hypergraph structure. - - Args: - ``e_style`` (``str``): The style of hyperedges. The available styles are only ``'circle'``. Defaults to ``'circle'``. - ``v_label`` (``list``): The labels of vertices. Defaults to ``None``. - ``v_size`` (``float`` or ``list``): The size of vertices. Defaults to ``1.0``. - ``v_color`` (``str`` or ``list``): The `color <https://matplotlib.org/stable/gallery/color/named_colors.html>`_ of vertices. Defaults to ``'r'``. - ``v_line_width`` (``float`` or ``list``): The line width of vertices. Defaults to ``1.0``. - ``e_color`` (``str`` or ``list``): The `color <https://matplotlib.org/stable/gallery/color/named_colors.html>`_ of hyperedges. Defaults to ``'gray'``. - ``e_fill_color`` (``str`` or ``list``): The fill `color <https://matplotlib.org/stable/gallery/color/named_colors.html>`_ of hyperedges. Defaults to ``'whitesmoke'``. - ``e_line_width`` (``float`` or ``list``): The line width of hyperedges. Defaults to ``1.0``. - ``font_size`` (``float``): The font size of labels. Defaults to ``1.0``. - ``font_family`` (``str``): The font family of labels. Defaults to ``'sans-serif'``. - ``push_v_strength`` (``float``): The strength of pushing vertices. Defaults to ``1.0``. - ``push_e_strength`` (``float``): The strength of pushing hyperedges. Defaults to ``1.0``. - ``pull_e_strength`` (``float``): The strength of pulling hyperedges. Defaults to ``1.0``. - ``pull_center_strength`` (``float``): The strength of pulling vertices to the center. Defaults to ``1.0``. - """ - draw_hypergraph( - self, - e_style, - v_label, - v_size, - v_color, - v_line_width, - e_color, - e_fill_color, - e_line_width, - font_size, - font_family, - push_v_strength, - push_e_strength, - pull_e_strength, - pull_center_strength, - )
- -
[docs] def clear(self): - r"""Clear all hyperedges and caches from the hypergraph.""" - return super().clear()
- -
[docs] def clone(self) -> "Hypergraph": - r"""Return a copy of the hypergraph.""" - hg = Hypergraph(self.num_v, device=self.device) - hg._raw_groups = deepcopy(self._raw_groups) - hg.cache = deepcopy(self.cache) - hg.group_cache = deepcopy(self.group_cache) - return hg
- -
[docs] def to(self, device: torch.device): - r"""Move the hypergraph to the specified device. - - Args: - ``device`` (``torch.device``): The target device. - """ - return super().to(device)
- - # ===================================================================================== - # some construction functions -
[docs] @staticmethod - def from_state_dict(state_dict: dict): - r"""Load the hypergraph from the state dict. - - Args: - ``state_dict`` (``dict``): The state dict to load the hypergraph. - """ - _hg = Hypergraph(state_dict["num_v"]) - _hg._raw_groups = deepcopy(state_dict["raw_groups"]) - return _hg
- - @staticmethod - def _e_list_from_feature_kNN(features: torch.Tensor, k: int): - import scipy - - r"""Construct hyperedges from the feature matrix. Each hyperedge in the hypergraph is constructed by the central vertex ans its :math:`k-1` neighbor vertices. - - Args: - ``features`` (``torch.Tensor``): The feature matrix. - ``k`` (``int``): The number of nearest neighbors. - """ - features = features.cpu().numpy() - assert features.ndim == 2, "The feature matrix should be 2-D." - assert k <= features.shape[0], ( - "The number of nearest neighbors should be less than or equal to the number" - " of vertices." - ) - tree = scipy.spatial.cKDTree(features) - _, nbr_array = tree.query(features, k=k) - return nbr_array.tolist() - -
[docs] @staticmethod - def from_feature_kNN( - features: torch.Tensor, k: int, device: torch.device = torch.device("cpu") - ): - r"""Construct the hypergraph from the feature matrix. Each hyperedge in the hypergraph is constructed by the central vertex ans its :math:`k-1` neighbor vertices. - - .. note:: - The constructed hypergraph is a k-uniform hypergraph. If the feature matrix has the size :math:`N \times C`, the number of vertices and hyperedges of the constructed hypergraph are both :math:`N`. - - Args: - ``features`` (``torch.Tensor``): The feature matrix. - ``k`` (``int``): The number of nearest neighbors. - ``device`` (``torch.device``, optional): The device to store the hypergraph. Defaults to ``torch.device('cpu')``. - """ - e_list = Hypergraph._e_list_from_feature_kNN(features, k) - hg = Hypergraph(features.shape[0], e_list, device=device) - return hg
- -
[docs] @staticmethod - def from_graph(graph, device: torch.device = torch.device("cpu")) -> "Hypergraph": - r"""Construct the hypergraph from the graph. Each edge in the graph is treated as a hyperedge in the constructed hypergraph. - - .. note:: - The construsted hypergraph is a 2-uniform hypergraph, and has the same number of vertices and edges/hyperedges as the graph. - - Args: - ``graph`` (``eg.Graph``): The graph to construct the hypergraph. - ``device`` (``torch.device``, optional): The device to store the hypergraph. Defaults to ``torch.device('cpu')``. - """ - e_list, e_weight = graph.e - hg = Hypergraph(len(graph.nodes), e_list, e_weight=e_weight, device=device) - return hg
- - @staticmethod - def _e_list_from_graph_kHop( - graph, - k: int, - only_kHop: bool = False, - ) -> List[tuple]: - r"""Construct the hyperedge list from the graph by k-Hop neighbors. Each hyperedge in the hypergraph is constructed by the central vertex and its :math:`k`-Hop neighbor vertices. - - .. note:: - If the graph have :math:`|\mathcal{V}|` vertices, the constructed hypergraph will have :math:`|\mathcal{V}|` vertices and equal to or less than :math:`|\mathcal{V}|` hyperedges. - - Args: - ``graph`` (``eg.Graph``): The graph to construct the hypergraph. - ``k`` (``int``): The number of hop neighbors. - ``only_kHop`` (``bool``, optional): If set to ``True``, only the central vertex and its :math:`k`-th Hop neighbors are used to construct the hyperedges. By default, the constructed hyperedge will include the central vertex and its [ :math:`1`-th, :math:`2`-th, :math:`\cdots`, :math:`k`-th ] Hop neighbors. Defaults to ``False``. - """ - assert ( - k >= 1 - ), "The number of hop neighbors should be larger than or equal to 1." - A_1, A_k = graph.A.clone(), graph.A.clone() - A_history = [] - for _ in range(k - 1): - A_k = torch.sparse.mm(A_k, A_1) - if not only_kHop: - A_history.append(A_k.clone()) - if not only_kHop: - A_k = A_1 - for A_ in A_history: - A_k = A_k + A_ - e_list = [ - tuple(set([v_idx] + A_k[v_idx]._indices().cpu().squeeze(0).tolist())) - for v_idx in range(len(graph.nodes)) - ] - return e_list - -
[docs] @staticmethod - def from_graph_kHop( - graph, - k: int, - only_kHop: bool = False, - device: torch.device = torch.device("cpu"), - ) -> "Hypergraph": - r"""Construct the hypergraph from the graph by k-Hop neighbors. Each hyperedge in the hypergraph is constructed by the central vertex and its :math:`k`-Hop neighbor vertices. - - .. note:: - If the graph have :math:`|\mathcal{V}|` vertices, the constructed hypergraph will have :math:`|\mathcal{V}|` vertices and equal to or less than :math:`|\mathcal{V}|` hyperedges. - - Args: - ``graph`` (``eg.Graph``): The graph to construct the hypergraph. - ``k`` (``int``): The number of hop neighbors. - ``only_kHop`` (``bool``): If set to ``True``, only the central vertex and its :math:`k`-th Hop neighbors are used to construct the hyperedges. By default, the constructed hyperedge will include the central vertex and its [ :math:`1`-th, :math:`2`-th, :math:`\cdots`, :math:`k`-th ] Hop neighbors. Defaults to ``False``. - ``device`` (``torch.device``, optional): The device to store the hypergraph. Defaults to ``torch.device('cpu')``. - """ - e_list = Hypergraph._e_list_from_graph_kHop(graph, k, only_kHop) - hg = Hypergraph(len(graph.nodes), e_list, device=device) - return hg
- -
[docs] def add_hyperedges( - self, - e_list: Union[List[int], List[List[int]]], - 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`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the vertices point to the hyperedges. - ``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"``, and ``"max"``. Defaults to ``"mean"``. - ``group_name`` (``str``, optional): The target hyperedge group to add these hyperedges. Defaults to the ``main`` hyperedge group. - """ - e_list = self._format_e_list(e_list) - if e_weight is None: - e_weight = [1.0] * len(e_list) - elif type(e_weight) in (int, float): - e_weight = [e_weight] - elif type(e_weight) is list: - pass - else: - raise TypeError( - "The type of e_weight should be float or list, but got" - f" {type(e_weight)}" - ) - assert len(e_list) == len( - e_weight - ), "The number of hyperedges and the number of weights are not equal." - - for _idx in range(len(e_list)): - self._add_hyperedge( - self._hyperedge_code(e_list[_idx], e_list[_idx]), - {"w_e": float(e_weight[_idx])}, - merge_op, - group_name, - ) - self._clear_cache(group_name)
- -
[docs] def add_hyperedges_from_feature_kNN( - self, feature: torch.Tensor, k: int, group_name: str = "main" - ): - r"""Add hyperedges from the feature matrix by k-NN. Each hyperedge is constructed by the central vertex and its :math:`k`-Nearest Neighbor vertices. - - Args: - ``features`` (``torch.Tensor``): The feature matrix. - ``k`` (``int``): The number of nearest neighbors. - ``group_name`` (``str``, optional): The target hyperedge group to add these hyperedges. Defaults to the ``main`` hyperedge group. - """ - assert feature.shape[0] == self.num_v, ( - "The number of vertices in the feature matrix is not equal to the number of" - " vertices in the hypergraph." - ) - e_list = Hypergraph._e_list_from_feature_kNN(feature, k) - self.add_hyperedges(e_list, group_name=group_name)
- -
[docs] def add_hyperedges_from_graph(self, graph, group_name: str = "main"): - r"""Add hyperedges from edges in the graph. Each edge in the graph is treated as a hyperedge. - - Args: - ``graph`` (``eg.Graph``): The graph to join the hypergraph. - ``group_name`` (``str``, optional): The target hyperedge group to add these hyperedges. Defaults to the ``main`` hyperedge group. - """ - assert self.num_v == len( - graph.nodes - ), "The number of vertices in the hypergraph and the graph are not equal." - e_list, e_weight = graph.e_both_side - self.add_hyperedges(e_list, e_weight=e_weight, group_name=group_name)
- -
[docs] def add_hyperedges_from_graph_kHop( - self, graph, k: int, only_kHop: bool = False, group_name: str = "main" - ): - r"""Add hyperedges from vertices and its k-Hop neighbors in the graph. Each hyperedge in the hypergraph is constructed by the central vertex and its :math:`k`-Hop neighbor vertices. - - .. note:: - If the graph have :math:`|\mathcal{V}|` vertices, the constructed hypergraph will have :math:`|\mathcal{V}|` vertices and equal to or less than :math:`|\mathcal{V}|` hyperedges. - - Args: - ``graph`` (``eg.Graph``): The graph to join the hypergraph. - ``k`` (``int``): The number of hop neighbors. - ``only_kHop`` (``bool``): If set to ``True``, only the central vertex and its :math:`k`-th Hop neighbors are used to construct the hyperedges. By default, the constructed hyperedge will include the central vertex and its [ :math:`1`-th, :math:`2`-th, :math:`\cdots`, :math:`k`-th ] Hop neighbors. Defaults to ``False``. - ``group_name`` (``str``, optional): The target hyperedge group to add these hyperedges. Defaults to the ``main`` hyperedge group. - """ - assert self.num_v == len( - graph.nodes - ), "The number of vertices in the hypergraph and the graph are not equal." - e_list = Hypergraph._e_list_from_graph_kHop(graph, k, only_kHop=only_kHop) - self.add_hyperedges(e_list, group_name=group_name)
- -
[docs] def remove_hyperedges( - self, - e_list: Union[List[int], List[List[int]]], - group_name: Optional[str] = None, - ): - r"""Remove the specified hyperedges from the hypergraph. - - Args: - ``e_list`` (``Union[List[int], List[List[int]]]``): A list of hyperedges describes how the vertices point to the hyperedges. - ``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 is None or group_name in self.group_names - ), "The specified group_name is not in existing hyperedge groups." - e_list = self._format_e_list(e_list) - if group_name is None: - for _idx in range(len(e_list)): - e_code = self._hyperedge_code(e_list[_idx], e_list[_idx]) - for name in self.group_names: - self._raw_groups[name].pop(e_code, None) - else: - for _idx in range(len(e_list)): - e_code = self._hyperedge_code(e_list[_idx], e_list[_idx]) - self._raw_groups[group_name].pop(e_code, None) - self._clear_cache(group_name)
- -
[docs] def remove_group(self, group_name: str): - r"""Remove the specified hyperedge group from the hypergraph. - - Args: - ``group_name`` (``str``): The name of the hyperedge group to remove. - """ - self._raw_groups.pop(group_name, None) - self._clear_cache(group_name)
- -
[docs] 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``. - """ - if ord == "uniform": - _raw_groups = {} - for name in self.group_names: - _raw_groups[name] = { - k: v - for k, v in self._raw_groups[name].items() - if random.random() > drop_rate - } - state_dict = { - "num_v": self.num_v, - "raw_groups": _raw_groups, - } - _hg = Hypergraph.from_state_dict(state_dict) - _hg = _hg.to(self.device) - else: - raise ValueError(f"Unknown drop order: {ord}.") - return _hg
- -
[docs] 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``. - """ - if ord == "uniform": - _raw_groups = {} - for name in self.group_names: - if name == group_name: - _raw_groups[name] = { - k: v - for k, v in self._raw_groups[name].items() - if random.random() > drop_rate - } - else: - _raw_groups[name] = self._raw_groups[name] - state_dict = { - "num_v": self.num_v, - "raw_groups": _raw_groups, - } - _hg = Hypergraph.from_state_dict(state_dict) - _hg = _hg.to(self.device) - else: - raise ValueError(f"Unknown drop order: {ord}.") - return _hg
- - # ===================================================================================== - # properties for representation - @property - def v(self) -> List[int]: - r"""Return the list of vertices.""" - return super().v - - @property - def e(self) -> Tuple[List[List[int]], List[float]]: - r"""Return all hyperedges and weights in the hypergraph.""" - if self.cache.get("e", None) is None: - e_list, e_weight = [], [] - for name in self.group_names: - _e = self.e_of_group(name) - e_list.extend(_e[0]) - e_weight.extend(_e[1]) - self.cache["e"] = (e_list, e_weight) - return self.cache["e"] - -
[docs] def e_of_group(self, group_name: str) -> Tuple[List[List[int]], List[float]]: - r"""Return all hyperedges and weights 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].get("e", None) is None: - e_list = [e_code[0] for e_code in self._raw_groups[group_name].keys()] - e_weight = [ - e_content["w_e"] for e_content in self._raw_groups[group_name].values() - ] - self.group_cache[group_name]["e"] = (e_list, e_weight) - return self.group_cache[group_name]["e"]
- - @property - def num_v(self) -> int: - r"""Return the number of vertices in the hypergraph.""" - return super().num_v - - @property - def num_e(self) -> int: - r"""Return the number of hyperedges in the hypergraph.""" - return super().num_e - -
[docs] def num_e_of_group(self, group_name: str) -> int: - r"""Return the number of hyperedges of the specified hyperedge group. - - Args: - ``group_name`` (``str``): The name of the specified hyperedge group. - """ - return super().num_e_of_group(group_name)
- - @property - def deg_v(self) -> List[int]: - r"""Return the degree list of each vertex.""" - return self.D_v._values().cpu().view(-1).numpy().tolist() - -
[docs] def deg_v_of_group(self, group_name: str) -> List[int]: - r"""Return the degree list of each vertex 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." - return self.D_v_of_group(group_name)._values().cpu().view(-1).numpy().tolist()
- - @property - def deg_e(self) -> List[int]: - r"""Return the degree list of each hyperedge.""" - return self.D_e._values().cpu().view(-1).numpy().tolist() - -
[docs] def deg_e_of_group(self, group_name: str) -> List[int]: - r"""Return the degree list of each hyperedge 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." - return self.D_e_of_group(group_name)._values().cpu().view(-1).numpy().tolist()
- -
[docs] def nbr_e(self, v_idx: int) -> List[int]: - r"""Return the neighbor hyperedge list of the specified vertex. - - Args: - ``v_idx`` (``int``): The index of the vertex. - """ - return self.N_e(v_idx).cpu().numpy().tolist()
- -
[docs] def nbr_e_of_group(self, v_idx: int, group_name: str) -> List[int]: - r"""Return the neighbor hyperedge list of the specified vertex of the specified hyperedge group. - - Args: - ``v_idx`` (``int``): The index of the vertex. - ``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 self.N_e_of_group(v_idx, group_name).cpu().numpy().tolist()
- -
[docs] def nbr_v(self, e_idx: int) -> List[int]: - r"""Return the neighbor vertex list of the specified hyperedge. - - Args: - ``e_idx`` (``int``): The index of the hyperedge. - """ - return self.N_v(e_idx).cpu().numpy().tolist()
- -
[docs] def nbr_v_of_group(self, e_idx: int, group_name: str) -> List[int]: - r"""Return the neighbor vertex list of the specified hyperedge of the specified hyperedge group. - - Args: - ``e_idx`` (``int``): The index of the hyperedge. - ``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 self.N_v_of_group(e_idx, group_name).cpu().numpy().tolist()
- - @property - def num_groups(self) -> int: - r"""Return the number of hyperedge groups in the hypergraph.""" - return super().num_groups - - @property - def group_names(self) -> List[str]: - r"""Return the names of all hyperedge groups in the hypergraph.""" - return super().group_names - - # ===================================================================================== - # properties for deep learning - @property - def vars_for_DL(self) -> List[str]: - r"""Return a name list of available variables for deep learning in the hypergraph including - - Sparse Matrices: - - .. math:: - \mathbf{H}, \mathbf{H}^\top, \mathcal{L}_{sym}, \mathcal{L}_{rw} \mathcal{L}_{HGNN}, - - Sparse Diagnal Matrices: - - .. math:: - \mathbf{W}_e, \mathbf{D}_v, \mathbf{D}_v^{-1}, \mathbf{D}_v^{-\frac{1}{2}}, \mathbf{D}_e, \mathbf{D}_e^{-1}, - - Vectors: - - .. math:: - \overrightarrow{v2e}_{src}, \overrightarrow{v2e}_{dst}, \overrightarrow{v2e}_{weight},\\ - \overrightarrow{e2v}_{src}, \overrightarrow{e2v}_{dst}, \overrightarrow{e2v}_{weight} - - """ - return [ - "H", - "H_T", - "L_sym", - "L_rw", - "L_HGNN", - "W_e", - "D_v", - "D_v_neg_1", - "D_v_neg_1_2", - "D_e", - "D_e_neg_1", - "v2e_src", - "v2e_dst", - "v2e_weighte2v_src", - "e2v_dst", - "e2v_weight", - ] - - @property - def v2e_src(self) -> torch.Tensor: - r"""Return the source vertex index vector :math:`\overrightarrow{v2e}_{src}` of the connections (vertices point to hyperedges) in the hypergraph. - """ - return self.H_T._indices()[1].clone() - -
[docs] def v2e_src_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the source vertex index vector :math:`\overrightarrow{v2e}_{src}` of the connections (vertices point to 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 self.H_T_of_group(group_name)._indices()[1].clone()
- - @property - def v2e_dst(self) -> torch.Tensor: - r"""Return the destination hyperedge index vector :math:`\overrightarrow{v2e}_{dst}` of the connections (vertices point to hyperedges) in the hypergraph. - """ - return self.H_T._indices()[0].clone() - -
[docs] def v2e_dst_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the destination hyperedge index vector :math:`\overrightarrow{v2e}_{dst}` of the connections (vertices point to 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 self.H_T_of_group(group_name)._indices()[0].clone()
- - @property - def v2e_weight(self) -> torch.Tensor: - r"""Return the weight vector :math:`\overrightarrow{v2e}_{weight}` of the connections (vertices point to hyperedges) in the hypergraph. - """ - return self.H_T._values().clone() - -
[docs] def v2e_weight_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the weight vector :math:`\overrightarrow{v2e}_{weight}` of the connections (vertices point to 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 self.H_T_of_group(group_name)._values().clone()
- - @property - def e2v_src(self) -> torch.Tensor: - r"""Return the source hyperedge index vector :math:`\overrightarrow{e2v}_{src}` of the connections (hyperedges point to vertices) in the hypergraph. - """ - return self.H._indices()[1].clone() - -
[docs] def e2v_src_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the source hyperedge index vector :math:`\overrightarrow{e2v}_{src}` of the connections (hyperedges point to vertices) 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 self.H_of_group(group_name)._indices()[1].clone()
- - @property - def e2v_dst(self) -> torch.Tensor: - r"""Return the destination vertex index vector :math:`\overrightarrow{e2v}_{dst}` of the connections (hyperedges point to vertices) in the hypergraph. - """ - return self.H._indices()[0].clone() - -
[docs] def e2v_dst_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the destination vertex index vector :math:`\overrightarrow{e2v}_{dst}` of the connections (hyperedges point to vertices) 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 self.H_of_group(group_name)._indices()[0].clone()
- - @property - def e2v_weight(self) -> torch.Tensor: - r"""Return the weight vector :math:`\overrightarrow{e2v}_{weight}` of the connections (hyperedges point to vertices) in the hypergraph. - """ - return self.H._values().clone() - -
[docs] def e2v_weight_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the weight vector :math:`\overrightarrow{e2v}_{weight}` of the connections (hyperedges point to vertices) 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 self.H_of_group(group_name)._values().clone()
- - @property - def H(self) -> torch.Tensor: - r"""Return the hypergraph incidence matrix :math:`\mathbf{H}` with ``torch.Tensor`` format. - """ - if self.cache.get("H") is None: - self.cache["H"] = self.H_v2e - return self.cache["H"] - -
[docs] def H_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the hypergraph incidence matrix :math:`\mathbf{H}` of the specified hyperedge group with ``torch.Tensor`` format. - - 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") is None: - self.group_cache[group_name]["H"] = self.H_v2e_of_group(group_name) - return self.group_cache[group_name]["H"]
- - @property - def H_T(self) -> torch.Tensor: - r"""Return the transpose of the hypergraph incidence matrix :math:`\mathbf{H}^\top` with ``torch.Tensor`` format. - """ - if self.cache.get("H_T") is None: - self.cache["H_T"] = self.H.t() - return self.cache["H_T"] - -
[docs] def H_T_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the transpose of the hypergraph incidence matrix :math:`\mathbf{H}^\top` of the specified hyperedge group with ``torch.Tensor`` format. - - 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_T") is None: - self.group_cache[group_name]["H_T"] = self.H_of_group(group_name).t() - return self.group_cache[group_name]["H_T"]
- - @property - def W_e(self) -> torch.Tensor: - r"""Return the weight matrix :math:`\mathbf{W}_e` of hyperedges with ``torch.Tensor`` format. - """ - if self.cache.get("W_e") is None: - _tmp = [ - self.W_e_of_group(name)._values().clone() for name in self.group_names - ] - _tmp = torch.cat(_tmp, dim=0).view(-1) - _num_e = _tmp.size(0) - self.cache["W_e"] = torch.sparse_coo_tensor( - torch.arange(0, _num_e).view(1, -1).repeat(2, 1), - _tmp, - torch.Size([_num_e, _num_e]), - device=self.device, - ).coalesce() - return self.cache["W_e"] - -
[docs] def W_e_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the weight matrix :math:`\mathbf{W}_e` of hyperedges of the specified hyperedge group with ``torch.Tensor`` format. - - 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("W_e") is None: - _tmp = self._fetch_W_of_group(group_name).view(-1) - _num_e = _tmp.size(0) - self.group_cache[group_name]["W_e"] = torch.sparse_coo_tensor( - torch.arange(0, _num_e).view(1, -1).repeat(2, 1), - _tmp, - torch.Size([_num_e, _num_e]), - device=self.device, - ).coalesce() - return self.group_cache[group_name]["W_e"]
- - @property - def D_v(self) -> torch.Tensor: - r"""Return the vertex degree matrix :math:`\mathbf{D}_v` with ``torch.sparse_coo_tensor`` format. - """ - if self.cache.get("D_v") is None: - _tmp = [ - self.D_v_of_group(name)._values().clone() for name in self.group_names - ] - _tmp = torch.vstack(_tmp).sum(dim=0).view(-1) - self.cache["D_v"] = torch.sparse_coo_tensor( - torch.arange(0, self.num_v).view(1, -1).repeat(2, 1), - _tmp, - torch.Size([self.num_v, self.num_v]), - device=self.device, - ).coalesce() - return self.cache["D_v"] - -
[docs] def D_v_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the vertex degree matrix :math:`\mathbf{D}_v` of the specified hyperedge group with ``torch.sparse_coo_tensor`` format. - - 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("D_v") is None: - _tmp = ( - torch.sparse.sum(self.H_of_group(group_name), dim=1) - .to_dense() - .clone() - .view(-1) - ) - _num_v = _tmp.size(0) - self.group_cache[group_name]["D_v"] = torch.sparse_coo_tensor( - torch.arange(0, _num_v).view(1, -1).repeat(2, 1), - _tmp, - torch.Size([_num_v, _num_v]), - device=self.device, - ).coalesce() - return self.group_cache[group_name]["D_v"]
- - @property - def D_v_neg_1(self) -> torch.Tensor: - r"""Return the vertex degree matrix :math:`\mathbf{D}_v^{-1}` with ``torch.sparse_coo_tensor`` format. - """ - if self.cache.get("D_v_neg_1") is None: - _mat = self.D_v.clone() - _val = _mat._values() ** -1 - _val[torch.isinf(_val)] = 0 - self.cache["D_v_neg_1"] = torch.sparse_coo_tensor( - _mat._indices(), _val, _mat.size(), device=self.device - ).coalesce() - return self.cache["D_v_neg_1"] - -
[docs] def D_v_neg_1_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the vertex degree matrix :math:`\mathbf{D}_v^{-1}` of the specified hyperedge group with ``torch.sparse_coo_tensor`` format. - - 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("D_v_neg_1") is None: - _mat = self.D_v_of_group(group_name).clone() - _val = _mat._values() ** -1 - _val[torch.isinf(_val)] = 0 - self.group_cache[group_name]["D_v_neg_1"] = torch.sparse_coo_tensor( - _mat._indices(), _val, _mat.size(), device=self.device - ).coalesce() - return self.group_cache[group_name]["D_v_neg_1"]
- - @property - def D_v_neg_1_2(self) -> torch.Tensor: - r"""Return the vertex degree matrix :math:`\mathbf{D}_v^{-\frac{1}{2}}` with ``torch.sparse_coo_tensor`` format. - """ - if self.cache.get("D_v_neg_1_2") is None: - _mat = self.D_v.clone() - _val = _mat._values() ** -0.5 - _val[torch.isinf(_val)] = 0 - self.cache["D_v_neg_1_2"] = torch.sparse_coo_tensor( - _mat._indices(), _val, _mat.size(), device=self.device - ).coalesce() - return self.cache["D_v_neg_1_2"] - -
[docs] def D_v_neg_1_2_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the vertex degree matrix :math:`\mathbf{D}_v^{-\frac{1}{2}}` of the specified hyperedge group with ``torch.sparse_coo_tensor`` format. - - 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("D_v_neg_1_2") is None: - _mat = self.D_v_of_group(group_name).clone() - _val = _mat._values() ** -0.5 - _val[torch.isinf(_val)] = 0 - self.group_cache[group_name]["D_v_neg_1_2"] = torch.sparse_coo_tensor( - _mat._indices(), _val, _mat.size(), device=self.device - ).coalesce() - return self.group_cache[group_name]["D_v_neg_1_2"]
- - @property - def D_e(self) -> torch.Tensor: - r"""Return the hyperedge degree matrix :math:`\mathbf{D}_e` with ``torch.sparse_coo_tensor`` format. - """ - if self.cache.get("D_e") is None: - _tmp = [ - self.D_e_of_group(name)._values().clone() for name in self.group_names - ] - _tmp = torch.cat(_tmp, dim=0).view(-1) - _num_e = _tmp.size(0) - self.cache["D_e"] = torch.sparse_coo_tensor( - torch.arange(0, _num_e).view(1, -1).repeat(2, 1), - _tmp, - torch.Size([_num_e, _num_e]), - device=self.device, - ).coalesce() - return self.cache["D_e"] - -
[docs] def D_e_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the hyperedge degree matrix :math:`\mathbf{D}_e` of the specified hyperedge group with ``torch.sparse_coo_tensor`` format. - - 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("D_e") is None: - _tmp = ( - torch.sparse.sum(self.H_T_of_group(group_name), dim=1) - .to_dense() - .clone() - .view(-1) - ) - _num_e = _tmp.size(0) - self.group_cache[group_name]["D_e"] = torch.sparse_coo_tensor( - torch.arange(0, _num_e).view(1, -1).repeat(2, 1), - _tmp, - torch.Size([_num_e, _num_e]), - device=self.device, - ).coalesce() - return self.group_cache[group_name]["D_e"]
- - @property - def D_e_neg_1(self) -> torch.Tensor: - r"""Return the hyperedge degree matrix :math:`\mathbf{D}_e^{-1}` with ``torch.sparse_coo_tensor`` format. - """ - if self.cache.get("D_e_neg_1") is None: - _mat = self.D_e.clone() - _val = _mat._values() ** -1 - _val[torch.isinf(_val)] = 0 - self.cache["D_e_neg_1"] = torch.sparse_coo_tensor( - _mat._indices(), _val, _mat.size(), device=self.device - ).coalesce() - return self.cache["D_e_neg_1"] - -
[docs] def D_e_neg_1_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the hyperedge degree matrix :math:`\mathbf{D}_e^{-1}` of the specified hyperedge group with ``torch.sparse_coo_tensor`` format. - - 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("D_e_neg_1") is None: - _mat = self.D_e_of_group(group_name).clone() - _val = _mat._values() ** -1 - _val[torch.isinf(_val)] = 0 - self.group_cache[group_name]["D_e_neg_1"] = torch.sparse_coo_tensor( - _mat._indices(), _val, _mat.size(), device=self.device - ).coalesce() - return self.group_cache[group_name]["D_e_neg_1"]
- -
[docs] def N_e(self, v_idx: int) -> torch.Tensor: - r"""Return the neighbor hyperedges of the specified vertex with ``torch.Tensor`` format. - - .. note:: - The ``v_idx`` must be in the range of [0, :attr:`num_v`). - - Args: - ``v_idx`` (``int``): The index of the vertex. - """ - assert v_idx < self.num_v - _tmp, e_bias = [], 0 - for name in self.group_names: - _tmp.append(self.N_e_of_group(v_idx, name) + e_bias) - e_bias += self.num_e_of_group(name) - return torch.cat(_tmp, dim=0)
- -
[docs] def N_e_of_group(self, v_idx: int, group_name: str) -> torch.Tensor: - r"""Return the neighbor hyperedges of the specified vertex of the specified hyperedge group with ``torch.Tensor`` format. - - .. note:: - The ``v_idx`` must be in the range of [0, :attr:`num_v`). - - Args: - ``v_idx`` (``int``): The index of the vertex. - ``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." - assert v_idx < self.num_v - e_indices = self.H_of_group(group_name)[v_idx]._indices()[0] - return e_indices.clone()
- -
[docs] def N_v(self, e_idx: int) -> torch.Tensor: - r"""Return the neighbor vertices of the specified hyperedge with ``torch.Tensor`` format. - - .. note:: - The ``e_idx`` must be in the range of [0, :attr:`num_e`). - - Args: - ``e_idx`` (``int``): The index of the hyperedge. - """ - assert e_idx < self.num_e - for name in self.group_names: - if e_idx < self.num_e_of_group(name): - return self.N_v_of_group(e_idx, name) - else: - e_idx -= self.num_e_of_group(name)
- -
[docs] def N_v_of_group(self, e_idx: int, group_name: str) -> torch.Tensor: - r"""Return the neighbor vertices of the specified hyperedge of the specified hyperedge group with ``torch.Tensor`` format. - - .. note:: - The ``e_idx`` must be in the range of [0, :func:`num_e_of_group`). - - Args: - ``e_idx`` (``int``): The index of the hyperedge. - ``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." - assert e_idx < self.num_e_of_group(group_name) - v_indices = self.H_T_of_group(group_name)[e_idx]._indices()[0] - return v_indices.clone()
- - # ===================================================================================== - # spectral-based convolution/smoothing -
[docs] def smoothing(self, X: torch.Tensor, L: torch.Tensor, lamb: float) -> torch.Tensor: - return super().smoothing(X, L, lamb)
- - @property - def L_sym(self) -> torch.Tensor: - r"""Return the symmetric Laplacian matrix :math:`\mathcal{L}_{sym}` of the hypergraph with ``torch.sparse_coo_tensor`` format. - - .. math:: - \mathcal{L}_{sym} = \mathbf{I} - \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} \mathbf{H}^\top \mathbf{D}_v^{-\frac{1}{2}} - """ - if self.cache.get("L_sym") is None: - L_HGNN = self.L_HGNN.clone() - self.cache["L_sym"] = torch.sparse_coo_tensor( - torch.hstack( - [ - torch.arange(0, self.num_v).view(1, -1).repeat(2, 1), - L_HGNN._indices(), - ] - ), - torch.hstack([torch.ones(self.num_v), -L_HGNN._values()]), - torch.Size([self.num_v, self.num_v]), - device=self.device, - ).coalesce() - return self.cache["L_sym"] - -
[docs] def L_sym_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the symmetric Laplacian matrix :math:`\mathcal{L}_{sym}` of the specified hyperedge group with ``torch.sparse_coo_tensor`` format. - - .. math:: - \mathcal{L}_{sym} = \mathbf{I} - \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} \mathbf{H}^\top \mathbf{D}_v^{-\frac{1}{2}} - - 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("L_sym") is None: - L_HGNN = self.L_HGNN_of_group(group_name).clone() - self.group_cache[group_name]["L_sym"] = torch.sparse_coo_tensor( - torch.hstack( - [ - torch.arange(0, self.num_v).view(1, -1).repeat(2, 1), - L_HGNN._indices(), - ] - ), - torch.hstack([torch.ones(self.num_v), -L_HGNN._values()]), - torch.Size([self.num_v, self.num_v]), - device=self.device, - ).coalesce() - return self.group_cache[group_name]["L_sym"]
- - @property - def L_rw(self) -> torch.Tensor: - r"""Return the random walk Laplacian matrix :math:`\mathcal{L}_{rw}` of the hypergraph with ``torch.sparse_coo_tensor`` format. - - .. math:: - \mathcal{L}_{rw} = \mathbf{I} - \mathbf{D}_v^{-1} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} \mathbf{H}^\top - """ - if self.cache.get("L_rw") is None: - _tmp = ( - self.D_v_neg_1.mm(self.H).mm(self.W_e).mm(self.D_e_neg_1).mm(self.H_T) - ) - self.cache["L_rw"] = ( - torch.sparse_coo_tensor( - torch.hstack( - [ - torch.arange(0, self.num_v).view(1, -1).repeat(2, 1), - _tmp._indices(), - ] - ), - torch.hstack([torch.ones(self.num_v), -_tmp._values()]), - torch.Size([self.num_v, self.num_v]), - device=self.device, - ) - .coalesce() - .clone() - ) - return self.cache["L_rw"] - -
[docs] def L_rw_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the random walk Laplacian matrix :math:`\mathcal{L}_{rw}` of the specified hyperedge group with ``torch.sparse_coo_tensor`` format. - - .. math:: - \mathcal{L}_{rw} = \mathbf{I} - \mathbf{D}_v^{-1} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} \mathbf{H}^\top - - 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("L_rw") is None: - _tmp = ( - self.D_v_neg_1_of_group(group_name) - .mm(self.H_of_group(group_name)) - .mm( - self.W_e_of_group(group_name), - ) - .mm( - self.D_e_neg_1_of_group(group_name), - ) - .mm( - self.H_T_of_group(group_name), - ) - ) - self.group_cache[group_name]["L_rw"] = ( - torch.sparse_coo_tensor( - torch.hstack( - [ - torch.arange(0, self.num_v).view(1, -1).repeat(2, 1), - _tmp._indices(), - ] - ), - torch.hstack([torch.ones(self.num_v), -_tmp._values()]), - torch.Size([self.num_v, self.num_v]), - device=self.device, - ) - .coalesce() - .clone() - ) - return self.group_cache[group_name]["L_rw"]
- - ## HGNN Laplacian smoothing - @property - def L_HGNN(self) -> torch.Tensor: - r"""Return the HGNN Laplacian matrix :math:`\mathcal{L}_{HGNN}` of the hypergraph with ``torch.sparse_coo_tensor`` format. - - .. math:: - \mathcal{L}_{HGNN} = \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} \mathbf{H}^\top \mathbf{D}_v^{-\frac{1}{2}} - """ - if self.cache.get("L_HGNN") is None: - _tmp = ( - self.D_v_neg_1_2.mm(self.H) - .mm(self.W_e) - .mm(self.D_e_neg_1) - .mm( - self.H_T, - ) - .mm(self.D_v_neg_1_2) - ) - self.cache["L_HGNN"] = _tmp.coalesce() - return self.cache["L_HGNN"] - -
[docs] def L_HGNN_of_group(self, group_name: str) -> torch.Tensor: - r"""Return the HGNN Laplacian matrix :math:`\mathcal{L}_{HGNN}` of the specified hyperedge group with ``torch.sparse_coo_tensor`` format. - - .. math:: - \mathcal{L}_{HGNN} = \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} \mathbf{H}^\top \mathbf{D}_v^{-\frac{1}{2}} - - 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("L_HGNN") is None: - _tmp = ( - self.D_v_neg_1_2_of_group(group_name) - .mm(self.H_of_group(group_name)) - .mm(self.W_e_of_group(group_name)) - .mm( - self.D_e_neg_1_of_group(group_name), - ) - .mm( - self.H_T_of_group(group_name), - ) - .mm( - self.D_v_neg_1_2_of_group(group_name), - ) - ) - self.group_cache[group_name]["L_HGNN"] = _tmp.coalesce() - return self.group_cache[group_name]["L_HGNN"]
- -
[docs] def smoothing_with_HGNN( - self, X: torch.Tensor, drop_rate: float = 0.0 - ) -> torch.Tensor: - r"""Return the smoothed feature matrix with the HGNN Laplacian matrix :math:`\mathcal{L}_{HGNN}`. - - .. math:: - \mathbf{X} = \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} \mathbf{H}^\top \mathbf{D}_v^{-\frac{1}{2}} \mathbf{X} - - Args: - ``X`` (``torch.Tensor``): The feature matrix. Size :math:`(|\mathcal{V}|, C)`. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - if self.device != X.device: - X = X.to(self.device) - if drop_rate > 0.0: - L_HGNN = sparse_dropout(self.L_HGNN, drop_rate) - else: - L_HGNN = self.L_HGNN - return L_HGNN.mm(X)
- -
[docs] def smoothing_with_HGNN_of_group( - self, group_name: str, X: torch.Tensor, drop_rate: float = 0.0 - ) -> torch.Tensor: - r"""Return the smoothed feature matrix with the HGNN Laplacian matrix :math:`\mathcal{L}_{HGNN}`. - - .. math:: - \mathbf{X} = \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} \mathbf{H}^\top \mathbf{D}_v^{-\frac{1}{2}} \mathbf{X} - - Args: - ``group_name`` (``str``): The name of the specified hyperedge group. - ``X`` (``torch.Tensor``): The feature matrix. Size :math:`(|\mathcal{V}|, C)`. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - assert ( - group_name in self.group_names - ), f"The specified {group_name} is not in existing hyperedge groups." - if self.device != X.device: - X = X.to(self.device) - if drop_rate > 0.0: - L_HGNN = sparse_dropout(self.L_HGNN_of_group(group_name), drop_rate) - else: - L_HGNN = self.L_HGNN_of_group(group_name) - return L_HGNN.mm(X)
- - # ===================================================================================== - # spatial-based convolution/message-passing - # general message passing functions -
[docs] 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - assert aggr in ["mean", "sum", "softmax_then_sum"] - if self.device != X.device: - self.to(X.device) - if v2e_weight is None: - if drop_rate > 0.0: - P = sparse_dropout(self.H_T, drop_rate) - else: - P = self.H_T - if aggr == "mean": - X = torch.sparse.mm(P, X) - X = torch.sparse.mm(self.D_e_neg_1, X) - elif aggr == "sum": - X = torch.sparse.mm(P, X) - elif aggr == "softmax_then_sum": - P = torch.sparse.softmax(P, dim=1) - X = torch.sparse.mm(P, X) - else: - raise ValueError(f"Unknown aggregation method {aggr}.") - else: - # init message path - assert ( - v2e_weight.shape[0] == self.v2e_weight.shape[0] - ), "The size of v2e_weight must be equal to the size of self.v2e_weight." - P = torch.sparse_coo_tensor( - self.H_T._indices(), v2e_weight, self.H_T.shape, device=self.device - ) - if drop_rate > 0.0: - P = sparse_dropout(P, drop_rate) - # message passing - if aggr == "mean": - X = torch.sparse.mm(P, X) - D_e_neg_1 = torch.sparse.sum(P, dim=1).to_dense().view(-1, 1) - D_e_neg_1[torch.isinf(D_e_neg_1)] = 0 - X = D_e_neg_1 * X - elif aggr == "sum": - X = torch.sparse.mm(P, X) - elif aggr == "softmax_then_sum": - P = torch.sparse.softmax(P, dim=1) - X = torch.sparse.mm(P, X) - else: - raise ValueError(f"Unknown aggregation method {aggr}.") - return X
- -
[docs] 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - assert ( - group_name in self.group_names - ), f"The specified {group_name} is not in existing hyperedge groups." - assert aggr in ["mean", "sum", "softmax_then_sum"] - if self.device != X.device: - self.to(X.device) - if v2e_weight is None: - if drop_rate > 0.0: - P = sparse_dropout(self.H_T_of_group(group_name), drop_rate) - else: - P = self.H_T_of_group(group_name) - if aggr == "mean": - X = torch.sparse.mm(P, X) - X = torch.sparse.mm(self.D_e_neg_1_of_group(group_name), X) - elif aggr == "sum": - X = torch.sparse.mm(P, X) - elif aggr == "softmax_then_sum": - P = torch.sparse.softmax(P, dim=1) - X = torch.sparse.mm(P, X) - else: - raise ValueError(f"Unknown aggregation method {aggr}.") - else: - # init message path - assert ( - v2e_weight.shape[0] == self.v2e_weight_of_group(group_name).shape[0] - ), ( - "The size of v2e_weight must be equal to the size of" - f" self.v2e_weight_of_group('{group_name}')." - ) - P = torch.sparse_coo_tensor( - self.H_T_of_group(group_name)._indices(), - v2e_weight, - self.H_T_of_group(group_name).shape, - device=self.device, - ) - if drop_rate > 0.0: - P = sparse_dropout(P, drop_rate) - # message passing - if aggr == "mean": - X = torch.sparse.mm(P, X) - D_e_neg_1 = torch.sparse.sum(P, dim=1).to_dense().view(-1, 1) - D_e_neg_1[torch.isinf(D_e_neg_1)] = 0 - X = D_e_neg_1 * X - elif aggr == "sum": - X = torch.sparse.mm(P, X) - elif aggr == "softmax_then_sum": - P = torch.sparse.softmax(P, dim=1) - X = torch.sparse.mm(P, X) - else: - raise ValueError(f"Unknown aggregation method {aggr}.") - return X
- -
[docs] 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``. - """ - if self.device != X.device: - self.to(X.device) - if e_weight is None: - X = torch.sparse.mm(self.W_e, X) - else: - e_weight = e_weight.view(-1, 1) - assert ( - e_weight.shape[0] == self.num_e - ), "The size of e_weight must be equal to the size of self.num_e." - X = e_weight * X - return X
- -
[docs] 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``. - """ - assert ( - group_name in self.group_names - ), f"The specified {group_name} is not in existing hyperedge groups." - if self.device != X.device: - self.to(X.device) - if e_weight is None: - X = torch.sparse.mm(self.W_e_of_group(group_name), X) - else: - e_weight = e_weight.view(-1, 1) - assert e_weight.shape[0] == self.num_e_of_group(group_name), ( - "The size of e_weight must be equal to the size of" - f" self.num_e_of_group('{group_name}')." - ) - X = e_weight * X - return X
- -
[docs] def v2e( - self, - X: torch.Tensor, - aggr: str = "mean", - v2e_weight: Optional[torch.Tensor] = None, - e_weight: Optional[torch.Tensor] = None, - drop_rate: float = 0.0, - ): - 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - X = self.v2e_aggregation(X, aggr, v2e_weight, drop_rate=drop_rate) - X = self.v2e_update(X, e_weight) - return X
- -
[docs] 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, - drop_rate: float = 0.0, - ): - 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - assert ( - group_name in self.group_names - ), f"The specified {group_name} is not in existing hyperedge groups." - X = self.v2e_aggregation_of_group( - group_name, X, aggr, v2e_weight, drop_rate=drop_rate - ) - X = self.v2e_update_of_group(group_name, X, e_weight) - return X
- -
[docs] def e2v_aggregation( - self, - X: torch.Tensor, - aggr: str = "mean", - e2v_weight: Optional[torch.Tensor] = None, - drop_rate: float = 0.0, - ): - 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - assert aggr in ["mean", "sum", "softmax_then_sum"] - if self.device != X.device: - self.to(X.device) - if e2v_weight is None: - if drop_rate > 0.0: - P = sparse_dropout(self.H, drop_rate) - else: - P = self.H - if aggr == "mean": - X = torch.sparse.mm(P, X) - X = torch.sparse.mm(self.D_v_neg_1, X) - elif aggr == "sum": - X = torch.sparse.mm(P, X) - elif aggr == "softmax_then_sum": - P = torch.sparse.softmax(P, dim=1) - X = torch.sparse.mm(P, X) - else: - raise ValueError(f"Unknown aggregation method: {aggr}") - else: - # init message path - assert ( - e2v_weight.shape[0] == self.e2v_weight.shape[0] - ), "The size of e2v_weight must be equal to the size of self.e2v_weight." - P = torch.sparse_coo_tensor( - self.H._indices(), e2v_weight, self.H.shape, device=self.device - ) - if drop_rate > 0.0: - P = sparse_dropout(P, drop_rate) - # message passing - if aggr == "mean": - X = torch.sparse.mm(P, X) - D_v_neg_1 = torch.sparse.sum(P, dim=1).to_dense().view(-1, 1) - D_v_neg_1[torch.isinf(D_v_neg_1)] = 0 - X = D_v_neg_1 * X - elif aggr == "sum": - X = torch.sparse.mm(P, X) - elif aggr == "softmax_then_sum": - P = torch.sparse.softmax(P, dim=1) - X = torch.sparse.mm(P, X) - else: - raise ValueError(f"Unknown aggregation method: {aggr}") - return X
- -
[docs] def e2v_aggregation_of_group( - self, - group_name: str, - X: torch.Tensor, - aggr: str = "mean", - e2v_weight: Optional[torch.Tensor] = None, - drop_rate: float = 0.0, - ): - 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - assert ( - group_name in self.group_names - ), f"The specified {group_name} is not in existing hyperedge groups." - assert aggr in ["mean", "sum", "softmax_then_sum"] - if self.device != X.device: - self.to(X.device) - if e2v_weight is None: - if drop_rate > 0.0: - P = sparse_dropout(self.H_of_group(group_name), drop_rate) - else: - P = self.H_of_group(group_name) - if aggr == "mean": - X = torch.sparse.mm(P, X) - X = torch.sparse.mm(self.D_v_neg_1_of_group[group_name], X) - elif aggr == "sum": - X = torch.sparse.mm(P, X) - elif aggr == "softmax_then_sum": - P = torch.sparse.softmax(P, dim=1) - X = torch.sparse.mm(P, X) - else: - raise ValueError(f"Unknown aggregation method: {aggr}") - else: - # init message path - assert ( - e2v_weight.shape[0] == self.e2v_weight_of_group[group_name].shape[0] - ), ( - "The size of e2v_weight must be equal to the size of" - f" self.e2v_weight_of_group('{group_name}')." - ) - P = torch.sparse_coo_tensor( - self.H_of_group[group_name]._indices(), - e2v_weight, - self.H_of_group[group_name].shape, - device=self.device, - ) - if drop_rate > 0.0: - P = sparse_dropout(P, drop_rate) - # message passing - if aggr == "mean": - X = torch.sparse.mm(P, X) - D_v_neg_1 = torch.sparse.sum(P, dim=1).to_dense().view(-1, 1) - D_v_neg_1[torch.isinf(D_v_neg_1)] = 0 - X = D_v_neg_1 * X - elif aggr == "sum": - X = torch.sparse.mm(P, X) - elif aggr == "softmax_then_sum": - P = torch.sparse.softmax(P, dim=1) - X = torch.sparse.mm(P, X) - else: - raise ValueError(f"Unknown aggregation method: {aggr}") - return X
- -
[docs] def e2v_update(self, X: torch.Tensor): - r"""Message update step of ``hyperedges to vertices``. - - Args: - ``X`` (``torch.Tensor``): Vertex feature matrix. Size :math:`(|\mathcal{V}|, C)`. - """ - if self.device != X.device: - self.to(X.device) - return X
- -
[docs] def e2v_update_of_group(self, group_name: str, X: torch.Tensor): - 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)`. - """ - assert ( - group_name in self.group_names - ), f"The specified {group_name} is not in existing hyperedge groups." - if self.device != X.device: - self.to(X.device) - return X
- -
[docs] def e2v( - self, - X: torch.Tensor, - aggr: str = "mean", - e2v_weight: Optional[torch.Tensor] = None, - drop_rate: float = 0.0, - ): - 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - X = self.e2v_aggregation(X, aggr, e2v_weight, drop_rate=drop_rate) - X = self.e2v_update(X) - return X
- -
[docs] def e2v_of_group( - self, - group_name: str, - X: torch.Tensor, - aggr: str = "mean", - e2v_weight: Optional[torch.Tensor] = None, - drop_rate: float = 0.0, - ): - 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - """ - assert ( - group_name in self.group_names - ), f"The specified {group_name} is not in existing hyperedge groups." - X = self.e2v_aggregation_of_group( - group_name, X, aggr, e2v_weight, drop_rate=drop_rate - ) - X = self.e2v_update_of_group(group_name, X) - return X
- -
[docs] def v2v( - self, - X: torch.Tensor, - aggr: str = "mean", - drop_rate: float = 0.0, - v2e_aggr: Optional[str] = None, - v2e_weight: Optional[torch.Tensor] = None, - v2e_drop_rate: Optional[float] = None, - e_weight: Optional[torch.Tensor] = None, - e2v_aggr: Optional[str] = None, - e2v_weight: Optional[torch.Tensor] = None, - e2v_drop_rate: Optional[float] = 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - ``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``. - ``v2e_drop_rate`` (``float``, optional): Dropout rate for hyperedges to vertices. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. If specified, it will override the ``drop_rate`` in ``e2v``. Default: ``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``. - ``e2v_drop_rate`` (``float``, optional): Dropout rate for vertices to hyperedges. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. If specified, it will override the ``drop_rate`` in ``v2e``. Default: ``None``. - """ - if v2e_aggr is None: - v2e_aggr = aggr - if e2v_aggr is None: - e2v_aggr = aggr - if v2e_drop_rate is None: - v2e_drop_rate = drop_rate - if e2v_drop_rate is None: - e2v_drop_rate = drop_rate - X = self.v2e(X, v2e_aggr, v2e_weight, e_weight, drop_rate=v2e_drop_rate) - X = self.e2v(X, e2v_aggr, e2v_weight, drop_rate=e2v_drop_rate) - return X
- -
[docs] def v2v_of_group( - self, - group_name: str, - X: torch.Tensor, - aggr: str = "mean", - drop_rate: float = 0.0, - v2e_aggr: Optional[str] = None, - v2e_weight: Optional[torch.Tensor] = None, - v2e_drop_rate: Optional[float] = None, - e_weight: Optional[torch.Tensor] = None, - e2v_aggr: Optional[str] = None, - e2v_weight: Optional[torch.Tensor] = None, - e2v_drop_rate: Optional[float] = 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``. - ``drop_rate`` (``float``): Dropout rate. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. Default: ``0.0``. - ``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``. - ``v2e_drop_rate`` (``float``, optional): Dropout rate for hyperedges to vertices. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. If specified, it will override the ``drop_rate`` in ``e2v_of_group``. Default: ``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``. - ``e2v_drop_rate`` (``float``, optional): Dropout rate for vertices to hyperedges. Randomly dropout the connections in incidence matrix with probability ``drop_rate``. If specified, it will override the ``drop_rate`` in ``v2e_of_group``. Default: ``None``. - """ - assert ( - group_name in self.group_names - ), f"The specified {group_name} is not in existing hyperedge groups." - if v2e_aggr is None: - v2e_aggr = aggr - if e2v_aggr is None: - e2v_aggr = aggr - if v2e_drop_rate is None: - v2e_drop_rate = drop_rate - if e2v_drop_rate is None: - e2v_drop_rate = drop_rate - X = self.v2e_of_group( - group_name, X, v2e_aggr, v2e_weight, e_weight, drop_rate=v2e_drop_rate - ) - X = self.e2v_of_group( - group_name, X, e2v_aggr, e2v_weight, drop_rate=e2v_drop_rate - ) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/classes/multigraph.html b/docs/_modules/easygraph/classes/multigraph.html deleted file mode 100644 index 3a61d1e2..00000000 --- a/docs/_modules/easygraph/classes/multigraph.html +++ /dev/null @@ -1,842 +0,0 @@ - - - - - - easygraph.classes.multigraph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.classes.multigraph

-"""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"]
-
-
-
[docs]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) - -
[docs] 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
- -
[docs] 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
- -
[docs] 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
- -
[docs] 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]
- -
[docs] 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
- -
[docs] 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 - -
[docs] 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 - -
[docs] def is_multigraph(self): - """Returns True if graph is a multigraph, False otherwise.""" - return True
- -
[docs] def is_directed(self): - """Returns True if graph is directed, False otherwise.""" - return False
- -
[docs] 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
- -
[docs] 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
- -
[docs] 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)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/classes/operation.html b/docs/_modules/easygraph/classes/operation.html deleted file mode 100644 index fb035109..00000000 --- a/docs/_modules/easygraph/classes/operation.html +++ /dev/null @@ -1,560 +0,0 @@ - - - - - - easygraph.classes.operation — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.classes.operation

-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",
-]
-
-
-
[docs]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
- - -
[docs]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)
- - -
[docs]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() if d > 0} - zero_indegree = [v for v, d in G.in_degree() 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") - - -
[docs]def topological_sort(G): - for generation in eg.topological_generations(G): - yield from generation
- - -
[docs]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))
- - -
[docs]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 -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/convert.html b/docs/_modules/easygraph/convert.html deleted file mode 100644 index c0a4460d..00000000 --- a/docs/_modules/easygraph/convert.html +++ /dev/null @@ -1,617 +0,0 @@ - - - - - - easygraph.convert — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.convert

-import warnings
-
-from collections.abc import Collection
-from collections.abc import Generator
-from collections.abc import Iterator
-from copy import deepcopy
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Iterable
-from typing import List
-from typing import Optional
-from typing import Union
-
-import easygraph as eg
-
-
-if TYPE_CHECKING:
-    import dgl
-    import networkx as nx
-    import torch_geometric
-
-    from easygraph import DiGraph
-    from easygraph import Graph
-
-__all__ = [
-    "from_dict_of_dicts",
-    "to_easygraph_graph",
-    "from_edgelist",
-    "from_dict_of_lists",
-    "from_networkx",
-    "from_dgl",
-    "from_pyg",
-    "to_networkx",
-    "to_dgl",
-    "to_pyg",
-]
-
-
-
[docs]def to_easygraph_graph(data, create_using=None, multigraph_input=False): - """Make a EasyGraph graph from a known data structure. - - The preferred way to call this is automatically - from the class constructor - - >>> d = {0: {1: {"weight": 1}}} # dict-of-dicts single edge (0,1) - >>> G = eg.Graph(d) - - instead of the equivalent - - >>> G = eg.from_dict_of_dicts(d) - - Parameters - ---------- - data : object to be converted - - Current known types are: - any EasyGraph graph - dict-of-dicts - dict-of-lists - container (e.g. set, list, tuple) of edges - iterator (e.g. itertools.chain) that produces edges - generator of edges - Pandas DataFrame (row per edge) - numpy matrix - numpy ndarray - scipy sparse matrix - pygraphviz agraph - - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - - multigraph_input : bool (default False) - If True and data is a dict_of_dicts, - try to create a multigraph assuming dict_of_dict_of_lists. - If data and create_using are both multigraphs then create - a multigraph from a multigraph. - - """ - - # EasyGraph graph type - if hasattr(data, "adj"): - try: - result = from_dict_of_dicts( - data.adj, - create_using=create_using, - multigraph_input=data.is_multigraph(), - ) - # data.graph should be dict-like - result.graph.update(data.graph) - # data.nodes should be dict-like - # result.add_node_from(data.nodes.items()) possible but - # for custom node_attr_dict_factory which may be hashable - # will be unexpected behavior - for n, dd in data.nodes.items(): - result._node[n].update(dd) - return result - except Exception as err: - raise eg.EasyGraphError("Input is not a correct EasyGraph graph.") from err - - # pygraphviz agraph - if hasattr(data, "is_strict"): - try: - return eg.from_pyGraphviz_agraph(data, create_using=create_using) - except Exception as err: - raise eg.EasyGraphError("Input is not a correct pygraphviz graph.") from err - - # dict of dicts/lists - if isinstance(data, dict): - try: - return from_dict_of_dicts( - data, create_using=create_using, multigraph_input=multigraph_input - ) - except Exception as err: - if multigraph_input is True: - raise eg.EasyGraphError( - f"converting multigraph_input raised:\n{type(err)}: {err}" - ) - try: - return from_dict_of_lists(data, create_using=create_using) - except Exception as err: - raise TypeError("Input is not known type.") from err - - # Pandas DataFrame - try: - import pandas as pd - - if isinstance(data, pd.DataFrame): - if data.shape[0] == data.shape[1]: - try: - return eg.from_pandas_adjacency(data, create_using=create_using) - except Exception as err: - msg = "Input is not a correct Pandas DataFrame adjacency matrix." - raise eg.EasyGraphError(msg) from err - else: - try: - return eg.from_pandas_edgelist( - data, edge_attr=True, create_using=create_using - ) - except Exception as err: - msg = "Input is not a correct Pandas DataFrame adjacency edge-list." - raise eg.EasyGraphError(msg) from err - except ImportError: - warnings.warn("pandas not found, skipping conversion test.", ImportWarning) - - # numpy matrix or ndarray - try: - import numpy as np - - if isinstance(data, np.ndarray): - try: - return eg.from_numpy_array(data, create_using=create_using) - except Exception as err: - raise eg.EasyGraphError( - "Input is not a correct numpy matrix or array." - ) from err - except ImportError: - warnings.warn("numpy not found, skipping conversion test.", ImportWarning) - - # scipy sparse matrix - any format - try: - if hasattr(data, "format"): - try: - return eg.from_scipy_sparse_matrix(data, create_using=create_using) - except Exception as err: - raise eg.EasyGraphError( - "Input is not a correct scipy sparse matrix type." - ) from err - except ImportError: - warnings.warn("scipy not found, skipping conversion test.", ImportWarning) - - # Note: most general check - should remain last in order of execution - # Includes containers (e.g. list, set, dict, etc.), generators, and - # iterators (e.g. itertools.chain) of edges - - if isinstance(data, (Collection, Generator, Iterator)): - try: - return from_edgelist(data, create_using=create_using) - except Exception as err: - raise eg.EasyGraphError("Input is not a valid edge list") from err - - raise eg.EasyGraphError("Input is not a known data type for conversion.")
- - -
[docs]def from_dict_of_lists(d, create_using=None): - G = eg.empty_graph(0, create_using) - G.add_nodes_from(d) - if G.is_multigraph() and not G.is_directed(): - # a dict_of_lists can't show multiedges. BUT for undirected graphs, - # each edge shows up twice in the dict_of_lists. - # So we need to treat this case separately. - seen = {} - for node, nbrlist in d.items(): - for nbr in nbrlist: - if nbr not in seen: - G.add_edge(node, nbr) - seen[node] = 1 # don't allow reverse edge to show up - else: - G.add_edges_from( - ((node, nbr) for node, nbrlist in d.items() for nbr in nbrlist) - ) - return G
- - -
[docs]def from_dict_of_dicts(d, create_using=None, multigraph_input=False): - G = eg.empty_graph(0, create_using) - G.add_nodes_from(d) - # does dict d represent a MultiGraph or MultiDiGraph? - if multigraph_input: - if G.is_directed(): - if G.is_multigraph(): - G.add_edges_from( - (u, v, key, data) - for u, nbrs in d.items() - for v, datadict in nbrs.items() - for key, data in datadict.items() - ) - else: - G.add_edges_from( - (u, v, data) - for u, nbrs in d.items() - for v, datadict in nbrs.items() - for key, data in datadict.items() - ) - else: # Undirected - if G.is_multigraph(): - seen = set() # don't add both directions of undirected graph - for u, nbrs in d.items(): - for v, datadict in nbrs.items(): - if (u, v) not in seen: - G.add_edges_from( - (u, v, key, data) for key, data in datadict.items() - ) - seen.add((v, u)) - else: - seen = set() # don't add both directions of undirected graph - for u, nbrs in d.items(): - for v, datadict in nbrs.items(): - if (u, v) not in seen: - G.add_edges_from( - (u, v, data) for key, data in datadict.items() - ) - seen.add((v, u)) - - else: # not a multigraph to multigraph transfer - if G.is_multigraph() and not G.is_directed(): - # d can have both representations u-v, v-u in dict. Only add one. - # We don't need this check for digraphs since we add both directions, - # or for Graph() since it is done implicitly (parallel edges not allowed) - seen = set() - for u, nbrs in d.items(): - for v, data in nbrs.items(): - if (u, v) not in seen: - G.add_edge(u, v, key=0) - G[u][v][0].update(data) - seen.add((v, u)) - else: - G.add_edges_from( - ((u, v, data) for u, nbrs in d.items() for v, data in nbrs.items()) - ) - return G
- - -
[docs]def from_edgelist(edgelist, create_using=None): - """Returns a graph from a list of edges. - - Parameters - ---------- - edgelist : list or iterator - Edge tuples - - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - - Examples - -------- - >>> edgelist = [(0, 1)] # single edge (0,1) - >>> G = eg.from_edgelist(edgelist) - - or - - >>> G = eg.Graph(edgelist) # use Graph constructor - - """ - G = eg.empty_graph(0, create_using) - G.add_edges_from(edgelist) - return G
- - -
[docs]def to_networkx(g: "Union[Graph, DiGraph]") -> "Union[nx.Graph, nx.DiGraph]": - """Convert an EasyGraph to a NetworkX graph. - - Args: - g (Union[Graph, DiGraph]): An EasyGraph graph - - Raises: - ImportError is raised if NetworkX is not installed. - - Returns: - Union[nx.Graph, nx.DiGraph]: Converted NetworkX graph - """ - # if load_func_name in di_load_functions_name: - try: - import networkx as nx - except ImportError: - raise ImportError("NetworkX not found. Please install it.") - if g.is_directed(): - G = nx.DiGraph() - else: - G = nx.Graph() - - # copy attributes - G.graph = deepcopy(g.graph) - - nodes_with_edges = set() - for v1, v2, _ in g.edges: - G.add_edge(v1, v2) - nodes_with_edges.add(v1) - nodes_with_edges.add(v2) - for node in set(g.nodes) - nodes_with_edges: - G.add_node(node) - return G
- - -
[docs]def from_networkx(g: "Union[nx.Graph, nx.DiGraph]") -> "Union[Graph, DiGraph]": - """Convert a NetworkX graph to an EasyGraph graph. - - Args: - g (Union[nx.Graph, nx.DiGraph]): A NetworkX graph - - Returns: - Union[Graph, DiGraph]: Converted EasyGraph graph - """ - # try: - # import networkx as nx - # except ImportError: - # raise ImportError("NetworkX not found. Please install it.") - if g.is_directed(): - G = eg.DiGraph() - else: - G = eg.Graph() - - # copy attributes - G.graph = deepcopy(g.graph) - - nodes_with_edges = set() - for v1, v2, _ in g.edges: - G.add_edge(v1, v2) - nodes_with_edges.add(v1) - nodes_with_edges.add(v2) - for node in set(g.nodes) - nodes_with_edges: - G.add_node(node) - return G
- - -
[docs]def to_dgl(g: "Union[Graph, DiGraph]"): - """Convert an EasyGraph graph to a DGL graph. - - Args: - g (Union[Graph, DiGraph]): An EasyGraph graph - - Raises: - ImportError: If DGL is not installed. - - Returns: - DGLGraph: Converted DGL graph - """ - try: - import dgl - except ImportError: - raise ImportError("DGL not found. Please install it.") - g_nx = to_networkx(g) - g_dgl = dgl.from_networkx(g_nx) - return g_dgl
- - -
[docs]def from_dgl(g) -> "Union[Graph, DiGraph]": - """Convert a DGL graph to an EasyGraph graph. - - Args: - g (DGLGraph): A DGL graph - - Raises: - ImportError: If DGL is not installed. - - Returns: - Union[Graph, DiGraph]: Converted EasyGraph graph - """ - try: - import dgl - except ImportError: - raise ImportError("DGL not found. Please install it.") - g_nx = dgl.to_networkx(g) - g_eg = from_networkx(g_nx) - return g_eg
- - -
[docs]def to_pyg( - G: Any, - group_node_attrs: Optional[Union[List[str], all]] = None, # type: ignore - group_edge_attrs: Optional[Union[List[str], all]] = None, # type: ignore -) -> "torch_geometric.data.Data": # type: ignore - r"""Converts a :obj:`easygraph.Graph` or :obj:`easygraph.DiGraph` to a - :class:`torch_geometric.data.Data` instance. - - Args: - G (easygraph.Graph or easygraph.DiGraph): A easygraph graph. - group_node_attrs (List[str] or all, optional): The node attributes to - be concatenated and added to :obj:`data.x`. (default: :obj:`None`) - group_edge_attrs (List[str] or all, optional): The edge attributes to - be concatenated and added to :obj:`data.edge_attr`. - (default: :obj:`None`) - - .. note:: - - All :attr:`group_node_attrs` and :attr:`group_edge_attrs` values must - be numeric. - - Examples: - - >>> import torch_geometric as pyg - - >>> pyg_to_networkx = pyg.utils.convert.to_networkx # type: ignore - >>> networkx_to_pyg = pyg.utils.convert.from_networkx # type: ignore - >>> Data = pyg.data.Data # type: ignore - >>> edge_index = torch.tensor([ - ... [0, 1, 1, 2, 2, 3], - ... [1, 0, 2, 1, 3, 2], - ... ]) - >>> data = Data(edge_index=edge_index, num_nodes=4) - >>> g = pyg_to_networkx(data) - >>> # A `Data` object is returned - >>> to_pyg(g) - Data(edge_index=[2, 6], num_nodes=4) - """ - try: - import torch_geometric as pyg - - pyg_to_networkx = pyg.utils.convert.to_networkx # type: ignore - networkx_to_pyg = pyg.utils.convert.from_networkx # type: ignore - except ImportError: - raise ImportError("pytorch_geometric not found. Please install it.") - - g_nx = to_networkx(G) - g_pyg = networkx_to_pyg(g_nx, group_node_attrs, group_edge_attrs) - return g_pyg
- - -
[docs]def from_pyg( - data: "torch_geometric.data.Data", # type: ignore - node_attrs: Optional[Iterable[str]] = None, - edge_attrs: Optional[Iterable[str]] = None, - graph_attrs: Optional[Iterable[str]] = None, - to_undirected: Optional[Union[bool, str]] = False, - remove_self_loops: bool = False, -) -> Any: - r"""Converts a :class:`torch_geometric.data.Data` instance to a - :obj:`easygraph.Graph` if :attr:`to_undirected` is set to :obj:`True`, or - a directed :obj:`easygraph.DiGraph` otherwise. - - Args: - data (torch_geometric.data.Data): The data object. - node_attrs (iterable of str, optional): The node attributes to be - copied. (default: :obj:`None`) - edge_attrs (iterable of str, optional): The edge attributes to be - copied. (default: :obj:`None`) - graph_attrs (iterable of str, optional): The graph attributes to be - copied. (default: :obj:`None`) - to_undirected (bool or str, optional): If set to :obj:`True` or - "upper", will return a :obj:`easygraph.Graph` instead of a - :obj:`easygraph.DiGraph`. The undirected graph will correspond to - the upper triangle of the corresponding adjacency matrix. - Similarly, if set to "lower", the undirected graph will correspond - to the lower triangle of the adjacency matrix. (default: - :obj:`False`) - remove_self_loops (bool, optional): If set to :obj:`True`, will not - include self loops in the resulting graph. (default: :obj:`False`) - - Examples: - - >>> import torch_geometric as pyg - - >>> Data = pyg.data.Data # type: ignore - >>> edge_index = torch.tensor([ - ... [0, 1, 1, 2, 2, 3], - ... [1, 0, 2, 1, 3, 2], - ... ]) - >>> data = Data(edge_index=edge_index, num_nodes=4) - >>> from_pyg(data) - <easygraph.classes.digraph.DiGraph at 0x2713fdb40d0> - - """ - - try: - import torch_geometric as pyg - - pyg_to_networkx = pyg.utils.convert.to_networkx # type: ignore - networkx_to_pyg = pyg.utils.convert.from_networkx # type: ignore - except ImportError: - raise ImportError("pytorch_geometric not found. Please install it.") - g_nx = pyg_to_networkx( - data, node_attrs, edge_attrs, graph_attrs, to_undirected, remove_self_loops - ) - g_eg = from_networkx(g_nx) - return g_eg
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datapipe/common.html b/docs/_modules/easygraph/datapipe/common.html deleted file mode 100644 index 11667bfa..00000000 --- a/docs/_modules/easygraph/datapipe/common.html +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - easygraph.datapipe.common — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.datapipe.common

-from typing import Any
-from typing import Callable
-from typing import List
-from typing import Union
-
-import numpy as np
-import scipy.sparse
-import torch
-
-
-
[docs]def to_tensor( - X: Union[list, np.ndarray, torch.Tensor, scipy.sparse.csr_matrix] -) -> torch.Tensor: - r"""Convert ``List``, ``numpy.ndarray``, ``scipy.sparse.csr_matrix`` to ``torch.Tensor``. - - Args: - ``X`` (``Union[List, np.ndarray, torch.Tensor, scipy.sparse.csr_matrix]``): Input. - - Examples: - >>> import easygraph.datapipe as dd - >>> X = [[0.1, 0.2, 0.5], - [0.5, 0.2, 0.3], - [0.3, 0.2, 0]] - >>> dd.to_tensor(X) - tensor([[0.1000, 0.2000, 0.5000], - [0.5000, 0.2000, 0.3000], - [0.3000, 0.2000, 0.0000]]) - """ - if isinstance(X, list): - X = torch.tensor(X) - elif isinstance(X, scipy.sparse.csr_matrix): - X = X.todense() - X = torch.tensor(X) - elif isinstance(X, scipy.sparse.coo_matrix): - X = X.todense() - X = torch.tensor(X) - elif isinstance(X, np.ndarray): - X = torch.tensor(X) - else: - X = torch.tensor(X) - return X.float()
- - -
[docs]def to_bool_tensor(X: Union[List, np.ndarray, torch.Tensor]) -> torch.BoolTensor: - r"""Convert ``List``, ``numpy.ndarray``, ``torch.Tensor`` to ``torch.BoolTensor``. - - Args: - ``X`` (``Union[List, np.ndarray, torch.Tensor]``): Input. - - Examples: - >>> import easygraph.datapipe as dd - >>> X = [[0.1, 0.2, 0.5], - [0.5, 0.2, 0.3], - [0.3, 0.2, 0]] - >>> dd.to_bool_tensor(X) - tensor([[ True, True, True], - [ True, True, True], - [ True, True, False]]) - """ - if isinstance(X, list): - X = torch.tensor(X) - elif isinstance(X, np.ndarray): - X = torch.tensor(X) - else: - X = torch.tensor(X) - return X.bool()
- - -
[docs]def to_long_tensor(X: Union[List, np.ndarray, torch.Tensor]) -> torch.LongTensor: - r"""Convert ``List``, ``numpy.ndarray``, ``torch.Tensor`` to ``torch.LongTensor``. - - Args: - ``X`` (``Union[List, np.ndarray, torch.Tensor]``): Input. - - Examples: - >>> import easygraph.datapipe as dd - >>> X = [[1, 2, 5], - [5, 2, 3], - [3, 2, 0]] - >>> dd.to_long_tensor(X) - tensor([[1, 2, 5], - [5, 2, 3], - [3, 2, 0]]) - """ - if isinstance(X, list): - X = torch.tensor(X) - elif isinstance(X, np.ndarray): - X = torch.tensor(X) - else: - X = torch.tensor(X) - return X.long()
- - -
[docs]def compose_pipes(*pipes: Callable) -> Callable: - r"""Compose datapipe functions. - - Args: - ``pipes`` (``Callable``): Datapipe functions to compose. - """ - - def composed_pipes(X: Any) -> torch.Tensor: - for pipe in pipes: - X = pipe(X) - return X - - return composed_pipes
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datapipe/loader.html b/docs/_modules/easygraph/datapipe/loader.html deleted file mode 100644 index 0a3a905c..00000000 --- a/docs/_modules/easygraph/datapipe/loader.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - easygraph.datapipe.loader — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.datapipe.loader

-import json
-import pickle as pkl
-import re
-
-from pathlib import Path
-from typing import Callable
-from typing import List
-from typing import Optional
-from typing import Union
-
-
-
[docs]def load_from_pickle( - file_path: Path, keys: Optional[Union[str, List[str]]] = None, **kwargs -): - r"""Load data from a pickle file. - - Args: - ``file_path`` (``Path``): The local path of the file. - ``keys`` (``Union[str, List[str]]``, optional): The keys of the data. Defaults to ``None``. - """ - if isinstance(file_path, list): - raise ValueError("This function only support loading data from a single file.") - with open(file_path, "rb") as f: - data = pkl.load(f, **kwargs) - if keys is None: - return data - elif isinstance(keys, str): - return data[keys] - else: - return {key: data[key] for key in keys}
- - -
[docs]def load_from_json(file_path: Path, **kwargs): - r"""Load data from a json file. - - Args: - ``file_path`` (``Path``): The local path of the file. - """ - with open(file_path, "r") as f: - data = json.load(f, **kwargs) - return data
- - -
[docs]def load_from_txt( - file_path: Path, - dtype: Union[str, Callable], - sep: str = ",| |\t", - ignore_header: int = 0, -): - r"""Load data from a txt file. - - .. note:: - The separator is a regular expression of ``re`` module. Multiple separators can be separated by ``|``. More details can refer to `re.split <https://docs.python.org/3/library/re.html#re.split>`_. - - Args: - ``file_path`` (``Path``): The local path of the file. - ``dtype`` (``Union[str, Callable]``): The data type of the data can be either a string or a callable function. - ``sep`` (``str``, optional): The separator of each line in the file. Defaults to ``",| |\t"``. - ``ignore_header`` (``int``, optional): The number of lines to ignore in the header of the file. Defaults to ``0``. - """ - cast_fun = ret_cast_fun(dtype) - file_path = Path(file_path) - assert file_path.exists(), f"{file_path} does not exist." - data = [] - with open(file_path, "r") as f: - for _ in range(ignore_header): - f.readline() - data = [ - list(map(cast_fun, re.split(sep, line.strip()))) for line in f.readlines() - ] - return data
- - -
[docs]def ret_cast_fun(dtype: Union[str, Callable]): - r"""Return the cast function of the data type. The supported data types are: ``int``, ``float``, ``str``. - - Args: - ``dtype`` (``Union[str, Callable]``): The data type of the data can be either a string or a callable function. - """ - if isinstance(dtype, str): - if dtype == "int": - return int - elif dtype == "float": - return float - elif dtype == "str": - return str - else: - raise ValueError("dtype must be one of 'int', 'float', 'str'.") - else: - return dtype
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datapipe/normalize.html b/docs/_modules/easygraph/datapipe/normalize.html deleted file mode 100644 index 157a73f0..00000000 --- a/docs/_modules/easygraph/datapipe/normalize.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - easygraph.datapipe.normalize — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.datapipe.normalize

-from typing import Optional
-from typing import Union
-
-import torch
-
-
-
[docs]def norm_ft(X: torch.Tensor, ord: Optional[Union[int, float]] = None) -> torch.Tensor: - r"""Normalize the input feature matrix with specified ``ord`` refer to pytorch's `torch.linalg.norm <https://pytorch.org/docs/stable/generated/torch.linalg.norm.html#torch.linalg.norm>`_ function. - - .. note:: - The input feature matrix is expected to be a 1D vector or a 2D tensor with shape (num_samples, num_features). - - Args: - ``X`` (``torch.Tensor``): The input feature. - ``ord`` (``Union[int, float]``, optional): The order of the norm can be either an ``int``, ``float``. If ``ord`` is ``None``, the norm is computed with the 2-norm. Defaults to ``None``. - - Examples: - >>> import easygraph.datapipe as dd - >>> import torch - >>> X = torch.tensor([ - [0.1, 0.2, 0.5], - [0.5, 0.2, 0.3], - [0.3, 0.2, 0] - ]) - >>> dd.norm_ft(X) - tensor([[0.1826, 0.3651, 0.9129], - [0.8111, 0.3244, 0.4867], - [0.8321, 0.5547, 0.0000]]) - """ - if X.dim() == 1: - X_norm = 1 / torch.linalg.norm(X, ord=ord) - X_norm[torch.isinf(X_norm)] = 0 - return X * X_norm - elif X.dim() == 2: - X_norm = 1 / torch.linalg.norm(X, ord=ord, dim=1, keepdim=True) - X_norm[torch.isinf(X_norm)] = 0 - return X * X_norm - else: - raise ValueError( - "The input feature matrix is expected to be a 1D verter or a 2D tensor with" - " shape (num_samples, num_features)." - )
- - -
[docs]def min_max_scaler(X: torch.Tensor, ft_min: float, ft_max: float) -> torch.Tensor: - r"""Normalize the input feature matrix with min-max scaling. - - Args: - ``X`` (``torch.Tensor``): The input feature. - ``ft_min`` (``float``): The minimum value of the output feature. - ``ft_max`` (``float``): The maximum value of the output feature. - - Examples: - >>> import easygraph.datapipe as dd - >>> import torch - >>> X = torch.tensor([ - [0.1, 0.2, 0.5], - [0.5, 0.2, 0.3], - [0.3, 0.2, 0.0] - ]) - >>> dd.min_max_scaler(X, -1, 1) - tensor([[-0.6000, -0.2000, 1.0000], - [ 1.0000, -0.2000, 0.2000], - [ 0.2000, -0.2000, -1.0000]]) - """ - assert ( - ft_min < ft_max - ), "The minimum value of the feature should be less than the maximum value." - X_min, X_max = X.min().item(), X.max().item() - X_range = X_max - X_min - scale_ = (ft_max - ft_min) / X_range - min_ = ft_min - X_min * scale_ - X = X * scale_ + min_ - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/citation_graph.html b/docs/_modules/easygraph/datasets/citation_graph.html deleted file mode 100644 index 38aa6f3c..00000000 --- a/docs/_modules/easygraph/datasets/citation_graph.html +++ /dev/null @@ -1,988 +0,0 @@ - - - - - - easygraph.datasets.citation_graph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.citation_graph

-"""Cora, citeseer, pubmed dataset.
-
-"""
-from __future__ import absolute_import
-
-import os
-import pickle as pkl
-import sys
-
-import easygraph as eg
-import numpy as np
-import scipy.sparse as sp
-
-from easygraph.classes.graph import Graph
-
-from .graph_dataset_base import EasyGraphBuiltinDataset
-from .utils import _get_dgl_url
-from .utils import data_type_dict
-from .utils import deprecate_property
-from .utils import generate_mask_tensor
-from .utils import nonzero_1d
-from .utils import tensor
-
-
-def _pickle_load(pkl_file):
-    if sys.version_info > (3, 0):
-        return pkl.load(pkl_file, encoding="latin1")
-    else:
-        return pkl.load(pkl_file)
-
-
-
[docs]class CitationGraphDataset(EasyGraphBuiltinDataset): - r"""The citation graph dataset, including Cora, CiteSeer and PubMed. - Nodes mean authors and edges mean citation relationships. - - Parameters - ----------- - name: str - name can be 'Cora', 'CiteSeer' or 'PubMed'. - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: True. - reverse_edge : bool - Whether to add reverse edges in graph. Default: True. - transform : callable, optional - A transform that takes in a :class:`~eg.Graph` object and returns - a transformed version. The :class:`~eg.Graph` object will be - transformed before every access. - reorder : bool - Whether to reorder the graph using :func:`~eg.reorder_graph`. Default: False. - """ - _urls = { - "cora_v2": "dataset/cora_v2.zip", - "citeseer": "dataset/citeSeer.zip", - "pubmed": "dataset/pubmed.zip", - } - - def __init__( - self, - name, - raw_dir=None, - force_reload=False, - verbose=True, - reverse_edge=True, - transform=None, - reorder=False, - ): - assert name.lower() in ["cora", "citeseer", "pubmed"] - - # Previously we use the pre-processing in pygcn (https://github.com/tkipf/pygcn) - # for Cora, which is slightly different from the one used in the GCN paper - if name.lower() == "cora": - name = "cora_v2" - - url = _get_dgl_url(self._urls[name]) - self._reverse_edge = reverse_edge - self._reorder = reorder - - super(CitationGraphDataset, self).__init__( - name, - url=url, - raw_dir=raw_dir, - force_reload=force_reload, - verbose=verbose, - transform=transform, - ) - -
[docs] def process(self): - """Loads input data from data directory and reorder graph for better locality - - ind.name.x => the feature vectors of the training instances as scipy.sparse.csr.csr_matrix object; - ind.name.tx => the feature vectors of the test instances as scipy.sparse.csr.csr_matrix object; - ind.name.allx => the feature vectors of both labeled and unlabeled training instances - (a superset of ind.name.x) as scipy.sparse.csr.csr_matrix object; - ind.name.y => the one-hot labels of the labeled training instances as numpy.ndarray object; - ind.name.ty => the one-hot labels of the test instances as numpy.ndarray object; - ind.name.ally => the labels for instances in ind.name.allx as numpy.ndarray object; - ind.name.graph => a dict in the format {index: [index_of_neighbor_nodes]} as collections.defaultdict - object; - ind.name.test.index => the indices of test instances in graph, for the inductive setting as list object. - """ - root = self.raw_path - objnames = ["x", "y", "tx", "ty", "allx", "ally", "graph"] - objects = [] - for i in range(len(objnames)): - with open("{}/ind.{}.{}".format(root, self.name, objnames[i]), "rb") as f: - objects.append(_pickle_load(f)) - - x, y, tx, ty, allx, ally, graph = tuple(objects) - test_idx_reorder = _parse_index_file( - "{}/ind.{}.test.index".format(root, self.name) - ) - test_idx_range = np.sort(test_idx_reorder) - - if self.name == "citeseer": - # Fix CiteSeer dataset (there are some isolated nodes in the graph) - # Find isolated nodes, add them as zero-vecs into the right position - test_idx_range_full = range( - min(test_idx_reorder), max(test_idx_reorder) + 1 - ) - tx_extended = sp.lil_matrix((len(test_idx_range_full), x.shape[1])) - tx_extended[test_idx_range - min(test_idx_range), :] = tx - tx = tx_extended - ty_extended = np.zeros((len(test_idx_range_full), y.shape[1])) - ty_extended[test_idx_range - min(test_idx_range), :] = ty - ty = ty_extended - - features = sp.vstack((allx, tx)).tolil() - features[test_idx_reorder, :] = features[test_idx_range, :] - - if self.reverse_edge: - g = eg.DiGraph(eg.from_dict_of_lists(graph)) - # g = from_networkx(graph) - else: - graph = eg.Graph(eg.from_dict_of_lists(graph)) - # edges = list(graph.edges()) - # u, v = map(list, zip(*edges)) - # g = dgl_graph((u, v)) - - onehot_labels = np.vstack((ally, ty)) - onehot_labels[test_idx_reorder, :] = onehot_labels[test_idx_range, :] - labels = np.argmax(onehot_labels, 1) - - idx_test = test_idx_range.tolist() - idx_train = range(len(y)) - idx_val = range(len(y), len(y) + 500) - - train_mask = generate_mask_tensor(_sample_mask(idx_train, labels.shape[0])) - val_mask = generate_mask_tensor(_sample_mask(idx_val, labels.shape[0])) - test_mask = generate_mask_tensor(_sample_mask(idx_test, labels.shape[0])) - - g.ndata["train_mask"] = train_mask - g.ndata["val_mask"] = val_mask - g.ndata["test_mask"] = test_mask - g.ndata["label"] = tensor(labels) - g.ndata["feat"] = tensor( - _preprocess_features(features), dtype=data_type_dict()["float32"] - ) - self._num_classes = onehot_labels.shape[1] - self._labels = labels - # if self._reorder: - # self._g = reorder_graph( - # g, node_permute_algo='rcmk', edge_permute_algo='dst', store_ids=False) - # else: - self._g = g - - if self.verbose: - print("Finished data loading and preprocessing.") - print(" NumNodes: {}".format(self._g.number_of_nodes())) - print(" NumEdges: {}".format(self._g.number_of_edges())) - print(" NumFeats: {}".format(self._g.ndata["feat"].shape[1])) - print(" NumClasses: {}".format(self.num_classes)) - print( - " NumTrainingSamples: {}".format( - nonzero_1d(self._g.ndata["train_mask"]).shape[0] - ) - ) - print( - " NumValidationSamples: {}".format( - nonzero_1d(self._g.ndata["val_mask"]).shape[0] - ) - ) - print( - " NumTestSamples: {}".format( - nonzero_1d(self._g.ndata["test_mask"]).shape[0] - ) - )
- -
[docs] def has_cache(self): - graph_path = os.path.join(self.save_path, self.save_name + ".bin") - info_path = os.path.join(self.save_path, self.save_name + ".pkl") - if os.path.exists(graph_path) and os.path.exists(info_path): - return True - - return False
- - # def save(self): - # """save the graph list and the labels""" - # graph_path = os.path.join(self.save_path, - # self.save_name + '.bin') - # info_path = os.path.join(self.save_path, - # self.save_name + '.pkl') - # save_graphs(str(graph_path), self._g) - # save_info(str(info_path), {'num_classes': self.num_classes}) - # - # def load(self): - # graph_path = os.path.join(self.save_path, - # self.save_name + '.bin') - # info_path = os.path.join(self.save_path, - # self.save_name + '.pkl') - # graphs, _ = load_graphs(str(graph_path)) - # - # info = load_info(str(info_path)) - # graph = graphs[0] - # self._g = graph - # # for compatibility - # graph = graph.clone() - # graph.ndata.pop('train_mask') - # graph.ndata.pop('val_mask') - # graph.ndata.pop('test_mask') - # graph.ndata.pop('feat') - # graph.ndata.pop('label') - # graph = to_networkx(graph) - # - # self._num_classes = info['num_classes'] - # self._g.ndata['train_mask'] = generate_mask_tensor(F.asnumpy(self._g.ndata['train_mask'])) - # self._g.ndata['val_mask'] = generate_mask_tensor(F.asnumpy(self._g.ndata['val_mask'])) - # self._g.ndata['test_mask'] = generate_mask_tensor(F.asnumpy(self._g.ndata['test_mask'])) - # # hack for mxnet compatibility - # - # if self.verbose: - # print(' NumNodes: {}'.format(self._g.number_of_nodes())) - # print(' NumEdges: {}'.format(self._g.number_of_edges())) - # print(' NumFeats: {}'.format(self._g.ndata['feat'].shape[1])) - # print(' NumClasses: {}'.format(self.num_classes)) - # print(' NumTrainingSamples: {}'.format( - # F.nonzero_1d(self._g.ndata['train_mask']).shape[0])) - # print(' NumValidationSamples: {}'.format( - # F.nonzero_1d(self._g.ndata['val_mask']).shape[0])) - # print(' NumTestSamples: {}'.format( - # F.nonzero_1d(self._g.ndata['test_mask']).shape[0])) - - def __getitem__(self, idx): - assert idx == 0, "This dataset has only one graph" - if self._transform is None: - return self._g - else: - return self._transform(self._g) - - def __len__(self): - return 1 - - @property - def save_name(self): - return self.name + "_dgl_graph" - - @property - def num_labels(self): - deprecate_property("dataset.num_labels", "dataset.num_classes") - return self.num_classes - - @property - def num_classes(self): - return self._num_classes - - """ Citation graph is used in many examples - We preserve these properties for compatibility. - """ - - @property - def reverse_edge(self): - return self._reverse_edge
- - -def _preprocess_features(features): - """Row-normalize feature matrix and convert to tuple representation""" - rowsum = np.asarray(features.sum(1)) - r_inv = np.power(rowsum, -1).flatten() - r_inv[np.isinf(r_inv)] = 0.0 - r_mat_inv = sp.diags(r_inv) - features = r_mat_inv.dot(features) - return np.asarray(features.todense()) - - -def _parse_index_file(filename): - """Parse index file.""" - index = [] - for line in open(filename): - index.append(int(line.strip())) - return index - - -def _sample_mask(idx, l): - """Create mask.""" - mask = np.zeros(l) - mask[idx] = 1 - return mask - - -
[docs]class CoraGraphDataset(CitationGraphDataset): - r"""Cora citation network dataset. - - Nodes mean paper and edges mean citation - relationships. Each node has a predefined - feature with 1433 dimensions. The dataset is - designed for the node classification task. - The task is to predict the category of - certain paper. - - Statistics: - - - Nodes: 2708 - - Edges: 10556 - - Number of Classes: 7 - - Label split: - - - Train: 140 - - Valid: 500 - - Test: 1000 - - Parameters - ---------- - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: True. - reverse_edge : bool - Whether to add reverse edges in graph. Default: True. - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - reorder : bool - Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False. - - Attributes - ---------- - num_classes: int - Number of label classes - - Notes - ----- - The node feature is row-normalized. - - Examples - -------- - >>> dataset = CoraGraphDataset() - >>> g = dataset[0] - >>> num_class = dataset.num_classes - >>> - >>> # get node feature - >>> feat = g.ndata['feat'] - >>> - >>> # get data split - >>> train_mask = g.ndata['train_mask'] - >>> val_mask = g.ndata['val_mask'] - >>> test_mask = g.ndata['test_mask'] - >>> - >>> # get labels - >>> label = g.ndata['label'] - - """ - - def __init__( - self, - raw_dir=None, - force_reload=False, - verbose=True, - reverse_edge=True, - transform=None, - reorder=False, - ): - name = "cora" - - super(CoraGraphDataset, self).__init__( - name, raw_dir, force_reload, verbose, reverse_edge, transform, reorder - ) - - def __getitem__(self, idx): - r"""Gets the graph object - - Parameters - ----------- - idx: int - Item index, CoraGraphDataset has only one graph object - - Return - ------ - :class:`dgl.DGLGraph` - - graph structure, node features and labels. - - - ``ndata['train_mask']``: mask for training node set - - ``ndata['val_mask']``: mask for validation node set - - ``ndata['test_mask']``: mask for test node set - - ``ndata['feat']``: node feature - - ``ndata['label']``: ground truth labels - """ - return super(CoraGraphDataset, self).__getitem__(idx) - - def __len__(self): - r"""The number of graphs in the dataset.""" - return super(CoraGraphDataset, self).__len__()
- - -
[docs]class CiteseerGraphDataset(CitationGraphDataset): - r"""Citeseer citation network dataset. - - Nodes mean scientific publications and edges - mean citation relationships. Each node has a - predefined feature with 3703 dimensions. The - dataset is designed for the node classification - task. The task is to predict the category of - certain publication. - - Statistics: - - - Nodes: 3327 - - Edges: 9228 - - Number of Classes: 6 - - Label Split: - - - Train: 120 - - Valid: 500 - - Test: 1000 - - Parameters - ----------- - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: True. - reverse_edge : bool - Whether to add reverse edges in graph. Default: True. - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - reorder : bool - Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False. - - Attributes - ---------- - num_classes: int - Number of label classes - - Notes - ----- - The node feature is row-normalized. - - In citeseer dataset, there are some isolated nodes in the graph. - These isolated nodes are added as zero-vecs into the right position. - - Examples - -------- - >>> dataset = CiteseerGraphDataset() - >>> g = dataset[0] - >>> num_class = dataset.num_classes - >>> - >>> # get node feature - >>> feat = g.ndata['feat'] - >>> - >>> # get data split - >>> train_mask = g.ndata['train_mask'] - >>> val_mask = g.ndata['val_mask'] - >>> test_mask = g.ndata['test_mask'] - >>> - >>> # get labels - >>> label = g.ndata['label'] - - """ - - def __init__( - self, - raw_dir=None, - force_reload=False, - verbose=True, - reverse_edge=True, - transform=None, - reorder=False, - ): - name = "citeseer" - - super(CiteseerGraphDataset, self).__init__( - name, raw_dir, force_reload, verbose, reverse_edge, transform, reorder - ) - - def __getitem__(self, idx): - r"""Gets the graph object - - Parameters - ----------- - idx: int - Item index, CiteseerGraphDataset has only one graph object - - Return - ------ - :class:`dgl.DGLGraph` - - graph structure, node features and labels. - - - ``ndata['train_mask']``: mask for training node set - - ``ndata['val_mask']``: mask for validation node set - - ``ndata['test_mask']``: mask for test node set - - ``ndata['feat']``: node feature - - ``ndata['label']``: ground truth labels - """ - return super(CiteseerGraphDataset, self).__getitem__(idx) - - def __len__(self): - r"""The number of graphs in the dataset.""" - return super(CiteseerGraphDataset, self).__len__()
- - -
[docs]class PubmedGraphDataset(CitationGraphDataset): - r"""Pubmed citation network dataset. - - Nodes mean scientific publications and edges - mean citation relationships. Each node has a - predefined feature with 500 dimensions. The - dataset is designed for the node classification - task. The task is to predict the category of - certain publication. - - Statistics: - - - Nodes: 19717 - - Edges: 88651 - - Number of Classes: 3 - - Label Split: - - - Train: 60 - - Valid: 500 - - Test: 1000 - - Parameters - ----------- - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: True. - reverse_edge : bool - Whether to add reverse edges in graph. Default: True. - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - reorder : bool - Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False. - - Attributes - ---------- - num_classes: int - Number of label classes - - Notes - ----- - The node feature is row-normalized. - - Examples - -------- - >>> dataset = PubmedGraphDataset() - >>> g = dataset[0] - >>> num_class = dataset.num_of_class - >>> - >>> # get node feature - >>> feat = g.ndata['feat'] - >>> - >>> # get data split - >>> train_mask = g.ndata['train_mask'] - >>> val_mask = g.ndata['val_mask'] - >>> test_mask = g.ndata['test_mask'] - >>> - >>> # get labels - >>> label = g.ndata['label'] - - """ - - def __init__( - self, - raw_dir=None, - force_reload=False, - verbose=True, - reverse_edge=True, - transform=None, - reorder=False, - ): - name = "pubmed" - - super(PubmedGraphDataset, self).__init__( - name, raw_dir, force_reload, verbose, reverse_edge, transform, reorder - ) - - def __getitem__(self, idx): - r"""Gets the graph object - - Parameters - ----------- - idx: int - Item index, PubmedGraphDataset has only one graph object - - Return - ------ - :class:`dgl.DGLGraph` - - graph structure, node features and labels. - - - ``ndata['train_mask']``: mask for training node set - - ``ndata['val_mask']``: mask for validation node set - - ``ndata['test_mask']``: mask for test node set - - ``ndata['feat']``: node feature - - ``ndata['label']``: ground truth labels - """ - return super(PubmedGraphDataset, self).__getitem__(idx) - - def __len__(self): - r"""The number of graphs in the dataset.""" - return super(PubmedGraphDataset, self).__len__()
- - -
[docs]def load_cora( - raw_dir=None, force_reload=False, verbose=True, reverse_edge=True, transform=None -): - """Get CoraGraphDataset - - Parameters - ----------- - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: True. - reverse_edge : bool - Whether to add reverse edges in graph. Default: True. - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - - Return - ------- - CoraGraphDataset - """ - data = CoraGraphDataset(raw_dir, force_reload, verbose, reverse_edge, transform) - return data
- - -
[docs]def load_citeseer( - raw_dir=None, force_reload=False, verbose=True, reverse_edge=True, transform=None -): - """Get CiteseerGraphDataset - - Parameters - ----------- - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: True. - reverse_edge : bool - Whether to add reverse edges in graph. Default: True. - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - - Return - ------- - CiteseerGraphDataset - """ - data = CiteseerGraphDataset(raw_dir, force_reload, verbose, reverse_edge, transform) - return data
- - -
[docs]def load_pubmed( - raw_dir=None, force_reload=False, verbose=True, reverse_edge=True, transform=None -): - """Get PubmedGraphDataset - - Parameters - ----------- - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: True. - reverse_edge : bool - Whether to add reverse edges in graph. Default: True. - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - - Return - ------- - PubmedGraphDataset - """ - data = PubmedGraphDataset(raw_dir, force_reload, verbose, reverse_edge, transform) - return data
- - -
[docs]class CoraBinary(EasyGraphBuiltinDataset): - """A mini-dataset for binary classification task using Cora. - - After loaded, it has following members: - - graphs : list of :class:`~dgl.DGLGraph` - pmpds : list of :class:`scipy.sparse.coo_matrix` - labels : list of :class:`numpy.ndarray` - - Parameters - ----------- - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose: bool - Whether to print out progress information. Default: True. - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - """ - - def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None): - name = "cora_binary" - url = _get_dgl_url("dataset/cora_binary.zip") - super(CoraBinary, self).__init__( - name, - url=url, - raw_dir=raw_dir, - force_reload=force_reload, - verbose=verbose, - transform=transform, - ) - -
[docs] def process(self): - root = self.raw_path - # load graphs - self.graphs = [] - with open("{}/graphs.txt".format(root), "r") as f: - elist = [] - for line in f.readlines(): - if line.startswith("graph"): - if len(elist) != 0: - self.graphs.append(Graph(elist)) - elist = [] - else: - u, v = line.strip().split(" ") - elist.append((int(u), int(v))) - if len(elist) != 0: - self.graphs.append(Graph(tuple(zip(*elist)))) - with open("{}/pmpds.pkl".format(root), "rb") as f: - self.pmpds = _pickle_load(f) - self.labels = [] - with open("{}/labels.txt".format(root), "r") as f: - cur = [] - for line in f.readlines(): - if line.startswith("graph"): - if len(cur) != 0: - self.labels.append(np.asarray(cur)) - cur = [] - else: - cur.append(int(line.strip())) - if len(cur) != 0: - self.labels.append(np.asarray(cur)) - # sanity check - assert len(self.graphs) == len(self.pmpds) - assert len(self.graphs) == len(self.labels)
- -
[docs] def has_cache(self): - graph_path = os.path.join(self.save_path, self.save_name + ".bin") - if os.path.exists(graph_path): - return True - - return False
- - # def save(self): - # """save the graph list and the labels""" - # graph_path = os.path.join(self.save_path, - # self.save_name + '.bin') - # labels = {} - # for i, label in enumerate(self.labels): - # labels['{}'.format(i)] = F.tensor(label) - # save_graphs(str(graph_path), self.graphs, labels) - # if self.verbose: - # print('Done saving data into cached files.') - # - # def load(self): - # graph_path = os.path.join(self.save_path, - # self.save_name + '.bin') - # self.graphs, labels = load_graphs(str(graph_path)) - # - # self.labels = [] - # for i in range(len(labels)): - # self.labels.append(F.asnumpy(labels['{}'.format(i)])) - # # load pmpds under self.raw_path - # with open("{}/pmpds.pkl".format(self.raw_path), 'rb') as f: - # self.pmpds = _pickle_load(f) - # if self.verbose: - # print('Done loading data into cached files.') - # # sanity check - # assert len(self.graphs) == len(self.pmpds) - # assert len(self.graphs) == len(self.labels) - - def __len__(self): - return len(self.graphs) - - def __getitem__(self, i): - r"""Gets the idx-th sample. - - Parameters - ----------- - idx : int - The sample index. - - Returns - ------- - (dgl.DGLGraph, scipy.sparse.coo_matrix, int) - The graph, scipy sparse coo_matrix and its label. - """ - if self._transform is None: - g = self.graphs[i] - else: - g = self._transform(self.graphs[i]) - return (g, self.pmpds[i], self.labels[i]) - - @property - def save_name(self): - return self.name + "_dgl_graph"
- - # @staticmethod - # def collate_fn(cur): - # graphs, pmpds, labels = zip(*cur) - # batched_graphs = batch.batch(graphs) - # batched_pmpds = sp.block_diag(pmpds) - # batched_labels = np.concatenate(labels, axis=0) - # return batched_graphs, batched_pmpds, batched_labels - - -def _normalize(mx): - """Row-normalize sparse matrix""" - rowsum = np.asarray(mx.sum(1)) - r_inv = np.power(rowsum, -1).flatten() - r_inv[np.isinf(r_inv)] = 0.0 - r_mat_inv = sp.diags(r_inv) - mx = r_mat_inv.dot(mx) - return mx - - -def _encode_onehot(labels): - classes = list(sorted(set(labels))) - classes_dict = {c: np.identity(len(classes))[i, :] for i, c in enumerate(classes)} - labels_onehot = np.asarray(list(map(classes_dict.get, labels)), dtype=np.int32) - return labels_onehot -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/dynamic/email_enron.html b/docs/_modules/easygraph/datasets/dynamic/email_enron.html deleted file mode 100644 index 7edf4c94..00000000 --- a/docs/_modules/easygraph/datasets/dynamic/email_enron.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - easygraph.datasets.dynamic.email_enron — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.dynamic.email_enron

-import json
-import os
-
-from easygraph.convert import dict_to_hypergraph
-from easygraph.datasets.dynamic.load_dataset import request_json_from_url
-from easygraph.datasets.graph_dataset_base import EasyGraphDataset
-from easygraph.datasets.utils import _get_eg_url
-from easygraph.datasets.utils import tensor
-
-
-
[docs]class Email_Enron(EasyGraphDataset): - _urls = { - "email-enron": ( - "easygraph-data-email-enron/-/raw/main/email-enron.json?inline=false" - ), - "email-eu": "easygraph-data-email-eu/-/raw/main/email-eu.json?inline=false", - } - - def __init__( - self, - raw_dir=None, - force_reload=False, - verbose=True, - transform=None, - save_dir="./", - ): - name = "email-enron" - self.url = _get_eg_url(self._urls[name]) - super(Email_Enron, self).__init__( - name=name, - url=self.url, - raw_dir=raw_dir, - force_reload=force_reload, - verbose=verbose, - transform=transform, - save_dir=save_dir, - ) - - @property - def url(self): - return self._url - - @property - def save_name(self): - return self.name - - def __getitem__(self, idx): - assert idx == 0, "This dataset has only one graph" - if self._transform is None: - return self._g - else: - return self._transform(self._g) - -
[docs] def load(self): - graph_path = os.path.join(self.save_path, self.save_name + ".json") - with open(graph_path, "r") as f: - self.load_data = json.load(f)
- -
[docs] def has_cache(self): - graph_path = os.path.join(self.save_path, self.save_name + ".json") - if os.path.exists(graph_path): - return True - return False
- -
[docs] def download(self): - if self.has_cache(): - self.load() - else: - root = self.raw_dir - data = request_json_from_url(self.url) - with open(os.path.join(root, self.save_name + ".json"), "w") as f: - json.dump(data, f) - self.load_data = data
- -
[docs] def process(self): - """Loads input data from data directory and transfer to target graph for better analysis - """ - - self._g, edge_feature_list = dict_to_hypergraph(self.load_data, is_dynamic=True) - - self._g.ndata["hyperedge_feature"] = tensor( - range(1, len(edge_feature_list) + 1) - )
- - @url.setter - def url(self, value): - self._url = value
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/dynamic/email_eu.html b/docs/_modules/easygraph/datasets/dynamic/email_eu.html deleted file mode 100644 index f1cdbbf7..00000000 --- a/docs/_modules/easygraph/datasets/dynamic/email_eu.html +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - easygraph.datasets.dynamic.email_eu — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.dynamic.email_eu

-import json
-import os
-
-from easygraph.convert import dict_to_hypergraph
-from easygraph.datasets.dynamic.load_dataset import request_json_from_url
-from easygraph.datasets.graph_dataset_base import EasyGraphDataset
-from easygraph.datasets.utils import _get_eg_url
-from easygraph.datasets.utils import tensor
-
-
-
[docs]class Email_Eu(EasyGraphDataset): - _urls = { - "email-eu": "easygraph-data-email-eu/-/raw/main/email-eu.json?inline=false", - } - - def __init__( - self, - raw_dir=None, - force_reload=False, - verbose=True, - transform=None, - save_dir="./", - ): - name = "email-eu" - self.url = _get_eg_url(self._urls[name]) - super(Email_Eu, self).__init__( - name=name, - url=self.url, - raw_dir=raw_dir, - force_reload=force_reload, - verbose=verbose, - transform=transform, - save_dir=save_dir, - ) - - @property - def url(self): - return self._url - - @property - def save_name(self): - return self.name - - def __getitem__(self, idx): - assert idx == 0, "This dataset has only one graph" - if self._transform is None: - return self._g - else: - return self._transform(self._g) - -
[docs] def load(self): - graph_path = os.path.join(self.save_path, self.save_name + ".json") - with open(graph_path, "r") as f: - self.load_data = json.load(f)
- -
[docs] def has_cache(self): - graph_path = os.path.join(self.save_path, self.save_name + ".json") - if os.path.exists(graph_path): - return True - return False
- -
[docs] def download(self): - if self.has_cache(): - self.load() - else: - root = self.raw_dir - data = request_json_from_url(self.url) - with open(os.path.join(root, self.save_name + ".json"), "w") as f: - json.dump(data, f) - self.load_data = data
- -
[docs] def process(self): - """Loads input data from data directory and transfer to target graph for better analysis - """ - self._g, edge_feature_list = dict_to_hypergraph(self.load_data, is_dynamic=True) - self._g.ndata["hyperedge_feature"] = tensor( - range(1, len(edge_feature_list) + 1) - )
- - @url.setter - def url(self, value): - self._url = value
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/dynamic/hospital_lyon.html b/docs/_modules/easygraph/datasets/dynamic/hospital_lyon.html deleted file mode 100644 index b39c14d3..00000000 --- a/docs/_modules/easygraph/datasets/dynamic/hospital_lyon.html +++ /dev/null @@ -1,245 +0,0 @@ - - - - - - easygraph.datasets.dynamic.hospital_lyon — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.dynamic.hospital_lyon

-import json
-import os
-
-from easygraph.classes.hypergraph import Hypergraph
-from easygraph.datasets.dynamic.load_dataset import request_json_from_url
-from easygraph.datasets.graph_dataset_base import EasyGraphDataset
-from easygraph.datasets.utils import _get_eg_url
-from easygraph.datasets.utils import tensor
-
-
-
[docs]class Hospital_Lyon(EasyGraphDataset): - _urls = { - "hospital_lyon": "easygraph-data-hospital-lyon/-/raw/main/hospital-lyon.json?ref_type=heads&inline=false", - } - - def __init__( - self, - raw_dir=None, - force_reload=False, - verbose=True, - transform=None, - save_dir="./", - ): - name = "hospital_lyon" - self.url = _get_eg_url(self._urls[name]) - super(Hospital_Lyon, self).__init__( - name=name, - url=self.url, - raw_dir=raw_dir, - force_reload=force_reload, - verbose=verbose, - transform=transform, - save_dir=save_dir, - ) - -
[docs] def preprocess(self, data, max_order=None, is_dynamic=True): - # The index of the nodes in this dataset are not continuous and therefore require special processing - timestamp_lst = list() - node_data = data["node-data"] - node_num = len(node_data) - G = Hypergraph(num_v=node_num) - id = 0 - name_dict = {} - for k, v in data["node-data"].items(): - name_dict[k] = id - v["name"] = k - G.v_property[id] = v - id = id + 1 - e_property_dict = data["edge-data"] - rows = [] - cols = [] - edge_flag_dict = {} - edge_id = 0 - for id, edge in data["edge-dict"].items(): - if max_order and len(edge) > max_order + 1: - continue - - try: - id = int(id) - except ValueError as e: - raise TypeError( - f"Failed to convert the edge with ID {id} to type int." - ) from e - - try: - edge = [name_dict[n] for n in edge] - rows.extend(edge) - cols.extend(len(edge) * [edge_id]) - edge_id += 1 - except ValueError as e: - raise TypeError(f"Failed to convert nodes to type int.") from e - if is_dynamic: - G.add_hyperedges( - e_list=edge, - e_property=e_property_dict[str(id)], - group_name=e_property_dict[str(id)]["timestamp"], - ) - timestamp_lst.append(e_property_dict[str(id)]["timestamp"]) - else: - G.add_hyperedges(e_list=edge, e_property=e_property_dict[str(id)]) - G._rows = rows - G._cols = cols - return G, timestamp_lst
- - @property - def url(self): - return self._url - - @property - def save_name(self): - return self.name - - def __getitem__(self, idx): - assert idx == 0, "This dataset has only one graph" - if self._transform is None: - return self._g - else: - return self._transform(self._g) - -
[docs] def load(self): - graph_path = os.path.join(self.save_path, self.save_name + ".json") - with open(graph_path, "r") as f: - self.load_data = json.load(f)
- -
[docs] def has_cache(self): - graph_path = os.path.join(self.save_path, self.save_name + ".json") - if os.path.exists(graph_path): - return True - return False
- -
[docs] def download(self): - if self.has_cache(): - self.load() - else: - root = self.raw_dir - data = request_json_from_url(self.url) - with open(os.path.join(root, self.save_name + ".json"), "w") as f: - json.dump(data, f) - self.load_data = data
- -
[docs] def process(self): - """Loads input data from data directory and transfer to target graph for better analysis - """ - - self._g, edge_feature_list = self.preprocess(self.load_data, is_dynamic=True) - self._g.ndata["hyperedge_feature"] = tensor( - range(1, len(edge_feature_list) + 1) - )
- - @url.setter - def url(self, value): - self._url = value
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/dynamic/load_dataset.html b/docs/_modules/easygraph/datasets/dynamic/load_dataset.html deleted file mode 100644 index f15e93a9..00000000 --- a/docs/_modules/easygraph/datasets/dynamic/load_dataset.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - easygraph.datasets.dynamic.load_dataset — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.dynamic.load_dataset

-import json
-import os
-
-from warnings import warn
-
-import requests
-
-from easygraph.convert import dict_to_hypergraph
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = [
-    "load_dynamic_hypergraph_dataset",
-]
-
-dataset_index_url = "https://gitlab.com/easy-graph/easygraph-data/-/raw/main/dataset_index.json?inline=false"
-
-
-def request_json_from_url(url):
-    try:
-        r = requests.get(url)
-    except requests.ConnectionError:
-        raise EasyGraphError("Connection Error!")
-
-    if r.ok:
-        return r.json()
-    else:
-        raise EasyGraphError(f"Error: HTTP response {r.status_code}")
-
-
-def _request_from_eg_data(dataset=None, cache=True):
-    """Request a dataset from eg-data.
-
-    Parameters
-    ----------
-    dataset : str, optional
-        Dataset name. Valid options are the top-level tags of the
-        index.json file in the xgi-data repository. If None, prints
-        the list of available datasets.
-    cache : bool, optional
-        Whether or not to cache the output
-
-    Returns
-    -------
-    Data
-        The requested data loaded from a json file.
-
-    Raises
-    ------
-    EasyGraphError
-        If the HTTP request is not successful or the dataset does not exist.
-
-
-    """
-
-    index_data = request_json_from_url(dataset_index_url)
-
-    key = dataset.lower()
-    if key not in index_data:
-        print("Valid dataset names:")
-        print(*index_data, sep="\n")
-        raise EasyGraphError("Must choose a valid dataset name!")
-
-    return request_json_from_url(index_data[key]["url"])
-
-
-
[docs]def load_dynamic_hypergraph_dataset( - dataset=None, - local_read=False, - path="", - max_order=None, -): - index_datasets = request_json_from_url(dataset_index_url) - if dataset is None: - print("Please refer to available list") - - print(*index_datasets, sep="\n") - return - - if local_read: - cfp = os.path.join(path, dataset + ".json") - if os.path.exists(cfp): - data = json.load(open(cfp, "r")) - return dict_to_hypergraph(data, max_order=max_order) - else: - warn( - f"No local copy was found at {cfp}. The data is requested " - "from the xgi-data repository instead. To download a local " - "copy, use `download_xgi_data`." - ) - data = _request_from_eg_data(dataset) - return dict_to_hypergraph( - data, max_order=max_order, is_dynamic=index_datasets[dataset]["is_dynamic"] - )
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/get_sample_graph.html b/docs/_modules/easygraph/datasets/get_sample_graph.html deleted file mode 100644 index fedb6693..00000000 --- a/docs/_modules/easygraph/datasets/get_sample_graph.html +++ /dev/null @@ -1,323 +0,0 @@ - - - - - - easygraph.datasets.get_sample_graph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.get_sample_graph

-import easygraph as eg
-
-
-# import progressbar
-
-
-__all__ = [
-    "get_graph_karateclub",
-    "get_graph_blogcatalog",
-    "get_graph_youtube",
-    "get_graph_flickr",
-]
-
-
-
[docs]def get_graph_karateclub(): - """Returns the undirected graph of Karate Club. - - Returns - ------- - get_graph_karateclub : easygraph.Graph - The undirected graph instance of karate club from dataset: - http://vlado.fmf.uni-lj.si/pub/networks/data/Ucinet/UciData.htm - - References - ---------- - .. [1] http://vlado.fmf.uni-lj.si/pub/networks/data/Ucinet/UciData.htm - - """ - all_members = set(range(34)) - club1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21} - # club2 = all_members - club1 - - G = eg.Graph(name="Zachary's Karate Club") - for node in all_members: - G.add_node(node + 1) - - zacharydat = """\ -0 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 1 0 0 -1 0 1 1 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 1 0 0 0 -1 1 0 1 0 0 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 -1 1 1 0 0 0 0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 -0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 -1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 1 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 1 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 -0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1 -0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 1 -0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 0 1 1 -0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 1 0 1 0 1 1 0 0 0 0 0 1 1 1 0 1 -0 0 0 0 0 0 0 0 1 1 0 0 0 1 1 1 0 0 1 1 1 0 1 1 0 0 1 1 1 1 1 1 1 0""" - - for row, line in enumerate(zacharydat.split("\n")): - thisrow = [int(b) for b in line.split()] - for col, entry in enumerate(thisrow): - if entry == 1: - G.add_edge(row + 1, col + 1) - - # Add the name of each member's club as a node attribute. - for v in G: - G.nodes[v]["club"] = "Mr. Hi" if v in club1 else "Officer" - return G
- - -
[docs]def get_graph_blogcatalog(): - """Returns the undirected graph of blogcatalog. - - Returns - ------- - get_graph_blogcatalog : easygraph.Graph - The undirected graph instance of blogcatalog from dataset: - https://github.com/phanein/deepwalk/blob/master/example_graphs/blogcatalog.mat - - References - ---------- - .. [1] https://github.com/phanein/deepwalk/blob/master/example_graphs/blogcatalog.mat - - """ - from scipy.io import loadmat - - def sparse2graph(x): - from collections import defaultdict - - G = defaultdict(lambda: set()) - cx = x.tocoo() - for i, j, v in zip(cx.row, cx.col, cx.data): - G[i].add(j) - return {str(k): [str(x) for x in v] for k, v in G.items()} - - mat = loadmat("./samples/blogcatalog.mat") - A = mat["network"] - data = sparse2graph(A) - - G = eg.Graph() - for u in data: - for v in data[u]: - G.add_edge(u, v) - - return G
- - -
[docs]def get_graph_youtube(): - """Returns the undirected graph of Youtube dataset. - - Returns - ------- - get_graph_youtube : easygraph.Graph - The undirected graph instance of Youtube from dataset: - http://socialnetworks.mpi-sws.mpg.de/data/youtube-links.txt.gz - - References - ---------- - .. [1] http://socialnetworks.mpi-sws.mpg.de/data/youtube-links.txt.gz - - """ - import gzip - - from urllib import request - - url = "http://socialnetworks.mpi-sws.mpg.de/data/youtube-links.txt.gz" - zipped_data_path = "./samples/youtube-links.txt.gz" - unzipped_data_path = "./samples/youtube-links.txt" - - # Download .gz file - print("Downloading Youtube dataset...") - request.urlretrieve(url, zipped_data_path, _show_progress) - - # Unzip - unzipped_data = gzip.GzipFile(zipped_data_path) - open(unzipped_data_path, "wb+").write(unzipped_data.read()) - unzipped_data.close() - - # Returns graph - G = eg.Graph() - G.add_edges_from_file(file=unzipped_data_path) - return G
- - -
[docs]def get_graph_flickr(): - """Returns the undirected graph of Flickr dataset. - - Returns - ------- - get_graph_flickr : easygraph.Graph - The undirected graph instance of Flickr from dataset: - http://socialnetworks.mpi-sws.mpg.de/data/flickr-links.txt.gz - - References - ---------- - .. [1] http://socialnetworks.mpi-sws.mpg.de/data/flickr-links.txt.gz - - """ - import gzip - - from urllib import request - - url = "http://socialnetworks.mpi-sws.mpg.de/data/flickr-links.txt.gz" - zipped_data_path = "./samples/flickr-links.txt.gz" - unzipped_data_path = "./samples/flickr-links.txt" - - # Download .gz file - print("Downloading Flickr dataset...") - request.urlretrieve(url, zipped_data_path, _show_progress) - - # Unzip - unzipped_data = gzip.GzipFile(zipped_data_path) - open(unzipped_data_path, "wb+").write(unzipped_data.read()) - unzipped_data.close() - - # Returns graph - G = eg.Graph() - G.add_edges_from_file(file=unzipped_data_path) - return G
- - -_pbar = None - - -def _show_progress(block_num, block_size, total_size): - global _pbar - if _pbar is None: - _pbar = progressbar.ProgressBar(maxval=total_size) - _pbar.start() - - downloaded = block_num * block_size - if downloaded < total_size: - _pbar.update(downloaded) - else: - _pbar.finish() - _pbar = None -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/gnn_benchmark.html b/docs/_modules/easygraph/datasets/gnn_benchmark.html deleted file mode 100644 index ebfa4dcf..00000000 --- a/docs/_modules/easygraph/datasets/gnn_benchmark.html +++ /dev/null @@ -1,329 +0,0 @@ - - - - - - easygraph.datasets.gnn_benchmark — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.gnn_benchmark

-import os
-
-import numpy as np
-import scipy.sparse as sp
-
-from easygraph.classes.graph import Graph
-
-from .graph_dataset_base import EasyGraphBuiltinDataset
-from .utils import _get_dgl_url
-from .utils import _set_labels
-from .utils import data_type_dict
-from .utils import tensor
-
-
-__all__ = [
-    "AmazonCoBuyComputerDataset",
-]
-
-
-class GNNBenchmarkDataset(EasyGraphBuiltinDataset):
-    r"""Base Class for GNN Benchmark dataset
-
-    Reference: https://github.com/shchur/gnn-benchmark#datasets
-    """
-
-    def __init__(
-        self, name, raw_dir=None, force_reload=False, verbose=True, transform=None
-    ):
-        _url = _get_dgl_url("dataset/" + name + ".zip")
-        super(GNNBenchmarkDataset, self).__init__(
-            name=name,
-            url=_url,
-            raw_dir=raw_dir,
-            force_reload=force_reload,
-            verbose=verbose,
-            transform=transform,
-        )
-
-    def process(self):
-        npz_path = os.path.join(self.raw_path, self.name + ".npz")
-        g = self._load_npz(npz_path)
-        # g = transforms.reorder_graph(
-        #     g, node_permute_algo='rcmk', edge_permute_algo='dst', store_ids=False)
-        self._graph = g
-        self._data = [g]
-        self._print_info()
-
-    def has_cache(self):
-        graph_path = os.path.join(self.save_path, "dgl_graph_v1.bin")
-        if os.path.exists(graph_path):
-            return True
-        return False
-
-    # def save(self):
-    #     graph_path = os.path.join(self.save_path, 'dgl_graph_v1.bin')
-    #     save_graphs(graph_path, self._graph)
-    #
-    # def load(self):
-    #     graph_path = os.path.join(self.save_path, 'dgl_graph_v1.bin')
-    #     graphs, _ = load_graphs(graph_path)
-    #     self._graph = graphs[0]
-    #     self._data = [graphs[0]]
-    #     self._print_info()
-
-    def _print_info(self):
-        if self.verbose:
-            print("  NumNodes: {}".format(self._graph.number_of_nodes()))
-            print("  NumEdges: {}".format(2 * self._graph.number_of_edges()))
-            print("  NumFeats: {}".format(self._graph.ndata["feat"].shape[-1]))
-            print("  NumbClasses: {}".format(self.num_classes))
-
-    def _load_npz(self, file_name):
-        with np.load(file_name, allow_pickle=True) as loader:
-            loader = dict(loader)
-            num_nodes = loader["adj_shape"][0]
-            adj_matrix = sp.csr_matrix(
-                (loader["adj_data"], loader["adj_indices"], loader["adj_indptr"]),
-                shape=loader["adj_shape"],
-            ).tocoo()
-
-            if "attr_data" in loader:
-                # Attributes are stored as a sparse CSR matrix
-                attr_matrix = sp.csr_matrix(
-                    (
-                        loader["attr_data"],
-                        loader["attr_indices"],
-                        loader["attr_indptr"],
-                    ),
-                    shape=loader["attr_shape"],
-                ).todense()
-            elif "attr_matrix" in loader:
-                # Attributes are stored as a (dense) np.ndarray
-                attr_matrix = loader["attr_matrix"]
-            else:
-                attr_matrix = None
-
-            if "labels_data" in loader:
-                # Labels are stored as a CSR matrix
-                labels = sp.csr_matrix(
-                    (
-                        loader["labels_data"],
-                        loader["labels_indices"],
-                        loader["labels_indptr"],
-                    ),
-                    shape=loader["labels_shape"],
-                ).todense()
-            elif "labels" in loader:
-                # Labels are stored as a numpy array
-                labels = loader["labels"]
-            else:
-                labels = None
-        if hasattr(adj_matrix, "format"):
-            print("can be generate eg!")
-        g = Graph(incoming_graph_data=adj_matrix)
-        # g = transforms.to_bidirected(g)
-        g = _set_labels(g, labels)
-        g.ndata["feat"] = tensor(attr_matrix, data_type_dict()["float32"])
-        g.ndata["label"] = tensor(labels, data_type_dict()["int64"])
-        return g
-
-    @property
-    def num_classes(self):
-        """Number of classes."""
-        raise NotImplementedError
-
-    def __getitem__(self, idx):
-        r"""Get graph by index
-
-        Parameters
-        ----------
-        idx : int
-            Item index
-
-        Returns
-        -------
-        :class:`dgl.DGLGraph`
-
-            The graph contains:
-
-            - ``ndata['feat']``: node features
-            - ``ndata['label']``: node labels
-        """
-        assert idx == 0, "This dataset has only one graph"
-        if self._transform is None:
-            return self._graph
-        else:
-            return self._transform(self._graph)
-
-    def __len__(self):
-        r"""Number of graphs in the dataset"""
-        return 1
-
-
-
[docs]class AmazonCoBuyComputerDataset(GNNBenchmarkDataset): - r"""'Computer' part of the AmazonCoBuy dataset for node classification task. - - Amazon Computers and Amazon Photo are segments of the Amazon co-purchase graph [McAuley et al., 2015], - where nodes represent goods, edges indicate that two goods are frequently bought together, node - features are bag-of-words encoded product reviews, and class labels are given by the product category. - - Reference: `<https://github.com/shchur/gnn-benchmark#datasets>`_ - - Statistics: - - - Nodes: 13,752 - - Edges: 491,722 (note that the original dataset has 245,778 edges but DGL adds - the reverse edges and remove the duplicates, hence with a different number) - - Number of classes: 10 - - Node feature size: 767 - - Parameters - ---------- - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.dgl/ - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: True. - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - - Attributes - ---------- - num_classes : int - Number of classes for each node. - - Examples - -------- - >>> data = AmazonCoBuyComputerDataset() - >>> g = data[0] - >>> num_class = data.num_classes - >>> feat = g.ndata['feat'] # get node feature - >>> label = g.ndata['label'] # get node labels - """ - - def __init__(self, raw_dir=None, force_reload=False, verbose=True, transform=None): - super(AmazonCoBuyComputerDataset, self).__init__( - name="amazon_co_buy_computer", - raw_dir=raw_dir, - force_reload=force_reload, - verbose=verbose, - transform=transform, - ) - - @property - def num_classes(self): - """Number of classes. - - Return - ------- - int - """ - return 10
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/graph_dataset_base.html b/docs/_modules/easygraph/datasets/graph_dataset_base.html deleted file mode 100644 index d23e9337..00000000 --- a/docs/_modules/easygraph/datasets/graph_dataset_base.html +++ /dev/null @@ -1,432 +0,0 @@ - - - - - - easygraph.datasets.graph_dataset_base — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.graph_dataset_base

-"""Basic EasyGraph Dataset
-"""
-
-from __future__ import absolute_import
-
-import abc
-import hashlib
-import os
-import sys
-import traceback
-
-from ..utils import retry_method_with_fix
-from .utils import download
-from .utils import extract_archive
-from .utils import get_download_dir
-from .utils import makedirs
-
-
-
[docs]class EasyGraphDataset(object): - r"""The basic EasyGraph dataset for creating graph datasets. - This class defines a basic template class for EasyGraph Dataset. - The following steps will be executed automatically: - - 1. Check whether there is a dataset cache on disk - (already processed and stored on the disk) by - invoking ``has_cache()``. If true, goto 5. - 2. Call ``download()`` to download the data if ``url`` is not None. - 3. Call ``process()`` to process the data. - 4. Call ``save()`` to save the processed dataset on disk and goto 6. - 5. Call ``load()`` to load the processed dataset from disk. - 6. Done. - - Users can overwrite these functions with their - own data processing logic. - - Parameters - ---------- - name : str - Name of the dataset - url : str - Url to download the raw dataset. Default: None - raw_dir : str - Specifying the directory that will store the - downloaded data or the directory that - already stores the input data. - Default: ~/.EasyGraphData/ - save_dir : str - Directory to save the processed dataset. - Default: same as raw_dir - hash_key : tuple - A tuple of values as the input for the hash function. - Users can distinguish instances (and their caches on the disk) - from the same dataset class by comparing the hash values. - Default: (), the corresponding hash value is ``'f9065fa7'``. - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - - """ - - def __init__( - self, - name, - url=None, - raw_dir=None, - save_dir=None, - hash_key=(), - force_reload=False, - verbose=False, - transform=None, - ): - self._name = name - self._url = url - self._force_reload = force_reload - self._verbose = verbose - self._hash_key = hash_key - self._hash = self._get_hash() - self._transform = transform - - # if no dir is provided, the default EasyGraph download dir is used. - if raw_dir is None: - self._raw_dir = get_download_dir() - else: - self._raw_dir = raw_dir - - if save_dir is None: - self._save_dir = self._raw_dir - else: - self._save_dir = save_dir - self._load() - -
[docs] def download(self): - r"""Overwrite to realize your own logic of downloading data. - - It is recommended to download the to the :obj:`self.raw_dir` - folder. Can be ignored if the dataset is - already in :obj:`self.raw_dir`. - """ - pass
- -
[docs] def save(self): - r"""Overwrite to realize your own logic of - saving the processed dataset into files. - - It is recommended to use ``dgl.data.utils.save_graphs`` - to save dgl graph into files and use - ``dgl.data.utils.save_info`` to save extra - information into files. - """ - pass
- -
[docs] def load(self): - r"""Overwrite to realize your own logic of - loading the saved dataset from files. - - It is recommended to use ``dgl.data.utils.load_graphs`` - to load dgl graph from files and use - ``dgl.data.utils.load_info`` to load extra information - into python dict object. - """ - pass
- -
[docs] @abc.abstractmethod - def process(self): - r"""Overwrite to realize your own logic of processing the input data.""" - pass
- -
[docs] def has_cache(self): - r"""Overwrite to realize your own logic of - deciding whether there exists a cached dataset. - - By default False. - """ - return False
- - @retry_method_with_fix(download) - def _download(self): - """Download dataset by calling ``self.download()`` - if the dataset does not exists under ``self.raw_path``. - - By default ``self.raw_path = os.path.join(self.raw_dir, self.name)`` - One can overwrite ``raw_path()`` function to change the path. - """ - - if os.path.exists(self.raw_path): # pragma: no cover - return - - makedirs(self.raw_dir) - self.download() - - def _load(self): - """Entry point from __init__ to load the dataset. - - If cache exists: - - - Load the dataset from saved dgl graph and information files. - - If loading process fails, re-download and process the dataset. - - else: - - - Download the dataset if needed. - - Process the dataset and build the dgl graph. - - Save the processed dataset into files. - """ - - load_flag = not self._force_reload and self.has_cache() - if load_flag: - try: - self.load() - self.process() - if self.verbose: - print("Done loading data from cached files.") - except KeyboardInterrupt: - raise - except: - load_flag = False - if self.verbose: - print(traceback.format_exc()) - print("Loading from cache failed, re-processing.") - - if not load_flag: - self._download() - self.process() - self.save() - if self.verbose: - print("Done saving data into cached files.") - - def _get_hash(self): - """Compute the hash of the input tuple - - Example - ------- - Assume `self._hash_key = (10, False, True)` - - >>> hash_value = self._get_hash() - >>> hash_value - 'a770b222' - """ - hash_func = hashlib.sha1() - hash_func.update(str(self._hash_key).encode("utf-8")) - return hash_func.hexdigest()[:8] - - @property - def url(self): - r"""Get url to download the raw dataset.""" - return self._url - - @property - def name(self): - r"""Name of the dataset.""" - return self._name - - @property - def raw_dir(self): - r"""Raw file directory contains the input data folder.""" - return self._raw_dir - - @property - def raw_path(self): - r"""Directory contains the input data files. - By default raw_path = os.path.join(self.raw_dir, self.name) - """ - return os.path.join(self.raw_dir, self.name) - - @property - def save_dir(self): - r"""Directory to save the processed dataset.""" - return self._save_dir - - @property - def save_path(self): - r"""Path to save the processed dataset.""" - return os.path.join(self._save_dir) - - @property - def verbose(self): - r"""Whether to print information.""" - return self._verbose - - @property - def hash(self): - r"""Hash value for the dataset and the setting.""" - return self._hash - - @abc.abstractmethod - def __getitem__(self, idx): - r"""Gets the data object at index.""" - pass - - @abc.abstractmethod - def __len__(self): - r"""The number of examples in the dataset.""" - pass - - def __repr__(self): - return f'Dataset("{self.name}"' + f" save_path={self.save_path})"
- - -
[docs]class EasyGraphBuiltinDataset(EasyGraphDataset): - r"""The Basic EasyGraph Builtin Dataset. - - Parameters - ---------- - name : str - Name of the dataset. - url : str - Url to download the raw dataset. - raw_dir : str - Specifying the directory that will store the - downloaded data or the directory that - already stores the input data. - Default: ~/.dgl/ - hash_key : tuple - A tuple of values as the input for the hash function. - Users can distinguish instances (and their caches on the disk) - from the same dataset class by comparing the hash values. - force_reload : bool - Whether to reload the dataset. Default: False - verbose : bool - Whether to print out progress information. Default: False - transform : callable, optional - A transform that takes in a :class:`~dgl.DGLGraph` object and returns - a transformed version. The :class:`~dgl.DGLGraph` object will be - transformed before every access. - """ - - def __init__( - self, - name, - url, - raw_dir=None, - hash_key=(), - force_reload=False, - verbose=True, - transform=None, - save_dir=None, - ): - super(EasyGraphBuiltinDataset, self).__init__( - name, - url=url, - raw_dir=raw_dir, - save_dir=save_dir, - hash_key=hash_key, - force_reload=force_reload, - verbose=verbose, - transform=transform, - ) - -
[docs] def download(self): - r"""Automatically download data and extract it.""" - if self.url is not None: - zip_file_path = os.path.join(self.raw_dir, self.name + ".zip") - download(self.url, path=zip_file_path) - extract_archive(zip_file_path, self.raw_path)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/House_Committees.html b/docs/_modules/easygraph/datasets/hypergraph/House_Committees.html deleted file mode 100644 index 3127656e..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/House_Committees.html +++ /dev/null @@ -1,329 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.House_Committees — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.House_Committees

-import requests
-
-from easygraph.utils.exception import EasyGraphError
-
-
-
[docs]def request_text_from_url(url): - """ - Requests text data from the specified URL. - - Args: - url (str): The URL from which to request the text data. - - Returns: - str: The text content of the response if the request is successful. - - Raises: - EasyGraphError: If a connection error occurs during the request or if the HTTP response status code - indicates a failure. - """ - try: - r = requests.get(url) - except requests.ConnectionError: - raise EasyGraphError("Connection Error!") - - if r.ok: - return r.text - else: - raise EasyGraphError(f"Error: HTTP response {r.status_code}")
- - -
[docs]class House_Committees: - """ - A class for loading and processing the House Committees hypergraph dataset. - - This class fetches hyperedge, node label, node name, and label name data from predefined URLs, - processes the data, and generates a hypergraph representation. It also provides access to various - dataset attributes through properties and indexing. - - Attributes: - data_root (str): The root URL for the data. If `data_root` is provided during initialization, - it is set to "https://"; otherwise, it is `None`. - hyperedges_path (str): The URL of the file containing hyperedge information. - node_labels_path (str): The URL of the file containing node label information. - node_names_path (str): The URL of the file containing node name information. - label_names_path (str): The URL of the file containing label name information. - _hyperedges (list): A list of tuples representing hyperedges. - _node_labels (list): A list of node labels. - _label_names (list): A list of label names. - _node_names (list): A list of node names. - _content (dict): A dictionary containing dataset statistics and data, including the number of - classes, vertices, edges, the edge list, and node labels. - """ - - def __init__(self, data_root=None): - """ - Initializes a new instance of the `House_Committees` class. - - Args: - data_root (str, optional): The root URL for the data. If provided, it is set to "https://"; - otherwise, it is `None`. Defaults to `None`. - """ - self.data_root = "https://" if data_root is not None else data_root - self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/hyperedges-house-committees.txt?inline=false" - self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/node-labels-house-committees.txt?ref_type=heads&inline=false" - self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false" - self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/label-names-house-committees.txt?ref_type=heads&inline=false" - self._hyperedges = [] - self._node_labels = [] - self._label_names = [] - self._node_names = [] - self.generate_hypergraph( - hyperedges_path=self.hyperedges_path, - node_labels_path=self.node_labels_path, - node_names_path=self.node_names_path, - label_names_path=self.label_names_path, - ) - - self._content = { - "num_classes": len(self._label_names), - "num_vertices": len(self._node_labels), - "num_edges": len(self._hyperedges), - "edge_list": self._hyperedges, - "labels": self._node_labels, - } - -
[docs] def process_label_txt(self, data_str, delimiter="\n", transform_fun=str): - """ - Processes a string containing label data into a list of transformed values. - - Args: - data_str (str): The input string containing label data. - delimiter (str, optional): The delimiter used to split the input string. Defaults to "\n". - transform_fun (callable, optional): A function used to transform each label value. - Defaults to the `str` function. - - Returns: - list: A list of transformed label values. - """ - data_str = data_str.strip() - data_lst = data_str.split(delimiter) - final_lst = [] - for data in data_lst: - data = data.strip() - data = transform_fun(data) - final_lst.append(data) - return final_lst
- - def __getitem__(self, key: str): - """ - Retrieves a value from the `_content` dictionary using the specified key. - - Args: - key (str): The key used to access the `_content` dictionary. - - Returns: - Any: The value corresponding to the key in the `_content` dictionary. - """ - return self._content[key] - - @property - def node_labels(self): - """ - Gets the list of node labels. - - Returns: - list: A list of node labels. - """ - return self._node_labels - - @property - def node_names(self): - """ - Gets the list of node names. - - Returns: - list: A list of node names. - """ - return self._node_names - - @property - def label_names(self): - """ - Gets the list of label names. - - Returns: - list: A list of label names. - """ - return self._label_names - - @property - def hyperedges(self): - """ - Gets the list of hyperedges. - - Returns: - list: A list of tuples representing hyperedges. - """ - return self._hyperedges - -
[docs] def generate_hypergraph( - self, - hyperedges_path=None, - node_labels_path=None, - node_names_path=None, - label_names_path=None, - ): - """ - Generates a hypergraph by fetching and processing data from the specified URLs. - - Args: - hyperedges_path (str, optional): The URL of the file containing hyperedge information. - Defaults to `None`. - node_labels_path (str, optional): The URL of the file containing node label information. - Defaults to `None`. - node_names_path (str, optional): The URL of the file containing node name information. - Defaults to `None`. - label_names_path (str, optional): The URL of the file containing label name information. - Defaults to `None`. - """ - - def fun(data): - """ - Converts a string to an integer and subtracts 1. - - Args: - data (str): The input string to be converted. - - Returns: - int: The converted integer value minus 1. - """ - data = int(data) - 1 - return data - - hyperedges_info = request_text_from_url(hyperedges_path) - hyperedges_info = hyperedges_info.strip() - hyperedges_lst = hyperedges_info.split("\n") - for hyperedge in hyperedges_lst: - hyperedge = hyperedge.strip() - hyperedge = [int(i) - 1 for i in hyperedge.split(",")] - self._hyperedges.append(tuple(hyperedge)) - # print(self.hyperedges) - - node_labels_info = request_text_from_url(node_labels_path) - - process_node_labels_info = self.process_label_txt( - node_labels_info, transform_fun=fun - ) - self._node_labels = process_node_labels_info - # print("process_node_labels_info:", process_node_labels_info) - node_names_info = request_text_from_url(node_names_path) - process_node_names_info = self.process_label_txt(node_names_info) - self._node_names = process_node_names_info - # print("process_node_names_info:", process_node_names_info) - label_names_info = request_text_from_url(label_names_path) - process_label_names_info = self.process_label_txt(label_names_info) - self._label_names = process_label_names_info
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/Yelp.html b/docs/_modules/easygraph/datasets/hypergraph/Yelp.html deleted file mode 100644 index 537c335d..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/Yelp.html +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.Yelp — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.Yelp

-from typing import Optional
-
-from easygraph.datapipe import load_from_pickle
-from easygraph.datapipe import to_long_tensor
-from easygraph.datapipe import to_tensor
-from easygraph.datasets.hypergraph.hypergraph_dataset_base import BaseData
-
-
-
[docs]class YelpRestaurant(BaseData): - r"""The Yelp-Restaurant dataset is a restaurant-review network dataset for node classification task. - - More details see the DHG or `YOU ARE ALLSET: A MULTISET LEARNING FRAMEWORK FOR HYPERGRAPH NEURAL NETWORKS <https://openreview.net/pdf?id=hpBTIv2uy_E>`_ paper. - - The content of the Yelp-Restaurant dataset includes the following: - - - ``num_classes``: The number of classes: :math:`11`. - - ``num_vertices``: The number of vertices: :math:`50,758`. - - ``num_edges``: The number of edges: :math:`679,302`. - - ``dim_features``: The dimension of features: :math:`1,862`. - - ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(50,758 \times 1,862)`. - - ``edge_list``: The edge list. ``List`` with length :math:`679,302`. - - ``labels``: The label list. ``torch.LongTensor`` with size :math:`(50,758, )`. - - ``state``: The state list. ``torch.LongTensor`` with size :math:`(50,758, )`. - - ``city``: The city list. ``torch.LongTensor`` with size :math:`(50,758, )`. - - Args: - ``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``. - """ - - def __init__(self, data_root: Optional[str] = None) -> None: - super().__init__("yelp_restaurant", data_root) - self._content = { - "num_classes": 11, - "num_vertices": 50758, - "num_edges": 679302, - "dim_features": 1862, - "features": { - "upon": [ - { - "filename": "features.pkl", - "md5": "cedc4443884477c2e626025411c44cd7", - } - ], - "loader": load_from_pickle, - "preprocess": [ - to_tensor, - ], - }, - "edge_list": { - "upon": [ - { - "filename": "edge_list.pkl", - "md5": "4b26eecaa22305dd10edcd6372eb49da", - } - ], - "loader": load_from_pickle, - }, - "labels": { - "upon": [ - { - "filename": "labels.pkl", - "md5": "1cdc1ed9fb1f57b2accaa42db214d4ef", - } - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - "state": { - "upon": [ - {"filename": "state.pkl", "md5": "eef3b835fad37409f29ad36539296b57"} - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - "city": { - "upon": [ - {"filename": "city.pkl", "md5": "8302b167262b23067698e865cacd0b17"} - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - }
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/cat_edge_Cooking.html b/docs/_modules/easygraph/datasets/hypergraph/cat_edge_Cooking.html deleted file mode 100644 index 1bf9ee0c..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/cat_edge_Cooking.html +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.cat_edge_Cooking — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.cat_edge_Cooking

-import requests
-
-from easygraph.utils.exception import EasyGraphError
-
-
-
[docs]def request_text_from_url(url): - try: - r = requests.get(url) - except requests.ConnectionError: - raise EasyGraphError("Connection Error!") - - if r.ok: - return r.text - else: - raise EasyGraphError(f"Error: HTTP response {r.status_code}")
- - -
[docs]class cat_edge_Cooking: - def __init__(self, data_root=None): - self.data_root = "https://" if data_root is not None else data_root - self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-cat-edge-cooking/-/raw/main/hyperedges.txt?inline=false" - self.edge_labels_path = "https://gitlab.com/easy-graph/easygraph-data-cat-edge-cooking/-/raw/main/hyperedge-labels.txt?ref_type=heads&inline=false" - self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-cat-edge-cooking/-/raw/main/main/node-labels.txt?ref_type=heads&inline=false" - self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-cat-edge-cooking/-/raw/main/hyperedge-label-identities.txt?ref_type=heads&inline=false" - # self.hyperedges_path = [] - # self.edge_labels_path = [] - # self.node_names_path = [] - # self.label_names_path = [] - self.generate_hypergraph( - hyperedges_path=self.hyperedges_path, - edge_labels_path=self.edge_labels_path, - node_names_path=self.node_names_path, - label_names_path=self.label_names_path, - ) - self._content = { - "num_classes": len(self._label_names), - "num_vertices": len(self._node_labels), - "num_edges": len(self._hyperedges), - "edge_list": self._hyperedges, - "labels": self._node_labels, - } - - def __getitem__(self, key: str): - return self._content[key] - -
[docs] def process_label_txt(self, data_str, delimiter="\n", transform_fun=str): - data_str = data_str.strip() - data_lst = data_str.split(delimiter) - final_lst = [] - for data in data_lst: - data = data.strip() - data = transform_fun(data) - final_lst.append(data) - return final_lst
- - @property - def edge_labels(self): - return self._edge_labels - - @property - def node_names(self): - return self._node_names - - @property - def label_names(self): - return self._label_names - - @property - def hyperedges(self): - return self._hyperedges - -
[docs] def generate_hypergraph( - self, - hyperedges_path=None, - node_labels_path=None, - node_names_path=None, - label_names_path=None, - ): - def fun(data): - data = int(data) - 1 - return data - - hyperedges_info = request_text_from_url(hyperedges_path) - hyperedges_info = hyperedges_info.strip() - hyperedges_lst = hyperedges_info.split("\n") - for hyperedge in hyperedges_lst: - hyperedge = hyperedge.strip() - hyperedge = [int(i) - 1 for i in hyperedge.split(" ")] - self._hyperedges.append(tuple(hyperedge)) - # print(self.hyperedges) - - edge_labels_info = request_text_from_url(self.edge_labels_path) - process_node_labels_info = self.process_label_txt( - node_labels_info, transform_fun=fun - ) - self._edge_labels = process_edge_labels_info() - - node_names_info = request_text_from_url(node_names_path) - process_node_names_info = self.process_label_txt(node_names_info) - self._node_names = process_node_names_info - - label_names_info = request_text_from_url(label_names_path) - process_label_names_info = self.process_label_txt(label_names_info) - self._label_names = process_label_names_info
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/coauthorship.html b/docs/_modules/easygraph/datasets/hypergraph/coauthorship.html deleted file mode 100644 index 05b75b05..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/coauthorship.html +++ /dev/null @@ -1,305 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.coauthorship — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.coauthorship

-from functools import partial
-from typing import Optional
-
-from easygraph.datapipe import load_from_pickle
-from easygraph.datapipe import norm_ft
-from easygraph.datapipe import to_bool_tensor
-from easygraph.datapipe import to_long_tensor
-from easygraph.datapipe import to_tensor
-from easygraph.datasets.hypergraph.hypergraph_dataset_base import BaseData
-
-
-__all__ = ["CoauthorshipCora", "CoauthorshipDBLP"]
-
-
-
[docs]class CoauthorshipCora(BaseData): - r"""The Co-authorship Cora dataset is a citation network dataset for vertex classification task. - More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper. - - The content of the Co-authorship Cora dataset includes the following: - - - ``num_classes``: The number of classes: :math:`7`. - - ``num_vertices``: The number of vertices: :math:`2,708`. - - ``num_edges``: The number of edges: :math:`1,072`. - - ``dim_features``: The dimension of features: :math:`1,433`. - - ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(2,708 \times 1,433)`. - - ``edge_list``: The edge list. ``List`` with length :math:`1,072`. - - ``labels``: The label list. ``torch.LongTensor`` with size :math:`(2,708, )`. - - ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(2,708, )`. - - ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(2,708, )`. - - ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(2,708, )`. - - Args: - ``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``. - """ - - def __init__(self, data_root: Optional[str] = None) -> None: - super().__init__("coauthorship_cora", data_root) - self._content = { - "num_classes": 7, - "num_vertices": 2708, - "num_edges": 1072, - "dim_features": 1433, - "features": { - "upon": [ - { - "filename": "features.pkl", - "md5": "14257c0e24b4eb741b469a351e524785", - } - ], - "loader": load_from_pickle, - "preprocess": [to_tensor, partial(norm_ft, ord=1)], - }, - "edge_list": { - "upon": [ - { - "filename": "edge_list.pkl", - "md5": "a17ff337f1b9099f5a9d4d670674e146", - } - ], - "loader": load_from_pickle, - }, - "labels": { - "upon": [ - { - "filename": "labels.pkl", - "md5": "c8d11c452e0be69f79a47dd839279117", - } - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - "train_mask": { - "upon": [ - { - "filename": "train_mask.pkl", - "md5": "111db6c6f986be2908378df7bdca7a9b", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "val_mask": { - "upon": [ - { - "filename": "val_mask.pkl", - "md5": "ffab1055193ffb2fe74822bb575d332a", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "test_mask": { - "upon": [ - { - "filename": "test_mask.pkl", - "md5": "ffab1055193ffb2fe74822bb575d332a", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - }
- - -
[docs]class CoauthorshipDBLP(BaseData): - r"""The Co-authorship DBLP dataset is a citation network dataset for vertex classification task. - More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper. - - The content of the Co-authorship DBLP dataset includes the following: - - - ``num_classes``: The number of classes: :math:`6`. - - ``num_vertices``: The number of vertices: :math:`41,302`. - - ``num_edges``: The number of edges: :math:`22,363`. - - ``dim_features``: The dimension of features: :math:`1,425`. - - ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(41,302 \times 1,425)`. - - ``edge_list``: The edge list. ``List`` with length :math:`22,363`. - - ``labels``: The label list. ``torch.LongTensor`` with size :math:`(41,302, )`. - - ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(41,302, )`. - - ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(41,302, )`. - - ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(41,302, )`. - - Args: - ``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to None. - """ - - def __init__(self, data_root: Optional[str] = None) -> None: - super().__init__("coauthorship_dblp", data_root) - self._content = { - "num_classes": 6, - "num_vertices": 41302, - "num_edges": 22363, - "dim_features": 1425, - "features": { - "upon": [ - { - "filename": "features.pkl", - "md5": "b78fd31b2586d1e19a40b3f6cd9cc2e7", - } - ], - "loader": load_from_pickle, - "preprocess": [to_tensor, partial(norm_ft, ord=1)], - }, - "edge_list": { - "upon": [ - { - "filename": "edge_list.pkl", - "md5": "c6bf5f9f3b9683bcc9b7bcc9eb8707d8", - } - ], - "loader": load_from_pickle, - }, - "labels": { - "upon": [ - { - "filename": "labels.pkl", - "md5": "2e7a792ea018028d582af8f02f2058ca", - } - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - "train_mask": { - "upon": [ - { - "filename": "train_mask.pkl", - "md5": "a842b795c7cac4c2f98a56cf599bc1de", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "val_mask": { - "upon": [ - { - "filename": "val_mask.pkl", - "md5": "2ec4b7df7c5e6b355067a22c391ad578", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "test_mask": { - "upon": [ - { - "filename": "test_mask.pkl", - "md5": "2ec4b7df7c5e6b355067a22c391ad578", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - }
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/cocitation.html b/docs/_modules/easygraph/datasets/hypergraph/cocitation.html deleted file mode 100644 index 44989aa0..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/cocitation.html +++ /dev/null @@ -1,392 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.cocitation — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.cocitation

-from functools import partial
-from typing import Optional
-
-from easygraph.datapipe import load_from_pickle
-from easygraph.datapipe import norm_ft
-from easygraph.datapipe import to_bool_tensor
-from easygraph.datapipe import to_long_tensor
-from easygraph.datapipe import to_tensor
-from easygraph.datasets.hypergraph.hypergraph_dataset_base import BaseData
-
-
-
[docs]class CocitationCora(BaseData): - r"""The Co-citation Cora dataset is a citation network dataset for vertex classification task. - More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper. - - The content of the Co-citation Cora dataset includes the following: - - - ``num_classes``: The number of classes: :math:`7`. - - ``num_vertices``: The number of vertices: :math:`2,708`. - - ``num_edges``: The number of edges: :math:`1,579`. - - ``dim_features``: The dimension of features: :math:`1,433`. - - ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(2,708 \times 1,433)`. - - ``edge_list``: The edge list. ``List`` with length :math:`1,579`. - - ``labels``: The label list. ``torch.LongTensor`` with size :math:`(2,708, )`. - - ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(2,708, )`. - - ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(2,708, )`. - - ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(2,708, )`. - - Args: - ``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``. - """ - - def __init__(self, data_root: Optional[str] = None) -> None: - super().__init__("cocitation_cora", data_root) - self._content = { - "num_classes": 7, - "num_vertices": 2708, - "num_edges": 1579, - "dim_features": 1433, - "features": { - "upon": [ - { - "filename": "features.pkl", - "md5": "14257c0e24b4eb741b469a351e524785", - } - ], - "loader": load_from_pickle, - "preprocess": [to_tensor, partial(norm_ft, ord=1)], - }, - "edge_list": { - "upon": [ - { - "filename": "edge_list.pkl", - "md5": "e43d1321880c8ecb2260d8fb7effd9ea", - } - ], - "loader": load_from_pickle, - }, - "labels": { - "upon": [ - { - "filename": "labels.pkl", - "md5": "c8d11c452e0be69f79a47dd839279117", - } - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - "train_mask": { - "upon": [ - { - "filename": "train_mask.pkl", - "md5": "111db6c6f986be2908378df7bdca7a9b", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "val_mask": { - "upon": [ - { - "filename": "val_mask.pkl", - "md5": "ffab1055193ffb2fe74822bb575d332a", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "test_mask": { - "upon": [ - { - "filename": "test_mask.pkl", - "md5": "ffab1055193ffb2fe74822bb575d332a", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - }
- - -
[docs]class CocitationCiteseer(BaseData): - r"""The Co-citation Citeseer dataset is a citation network dataset for vertex classification task. - More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper. - - The content of the Co-citation Citaseer dataset includes the following: - - - ``num_classes``: The number of classes: :math:`6`. - - ``num_vertices``: The number of vertices: :math:`3,312`. - - ``num_edges``: The number of edges: :math:`1,079`. - - ``dim_features``: The dimension of features: :math:`3,703`. - - ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(3,312 \times 3,703)`. - - ``edge_list``: The edge list. ``List`` with length :math:`1,079`. - - ``labels``: The label list. ``torch.LongTensor`` with size :math:`(3,312, )`. - - ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(3,312, )`. - - ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(3,312, )`. - - ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(3,312, )`. - - Args: - ``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``. - """ - - def __init__(self, data_root: Optional[str] = None) -> None: - super().__init__("cocitation_citeseer", data_root) - self._content = { - "num_classes": 6, - "num_vertices": 3312, - "num_edges": 1079, - "dim_features": 3703, - "features": { - "upon": [ - { - "filename": "features.pkl", - "md5": "1ee0dc89e0d5f5ac9187b55a407683e8", - } - ], - "loader": load_from_pickle, - "preprocess": [to_tensor, partial(norm_ft, ord=1)], - }, - "edge_list": { - "upon": [ - { - "filename": "edge_list.pkl", - "md5": "6687b2e96159c534a424253f536b49ae", - } - ], - "loader": load_from_pickle, - }, - "labels": { - "upon": [ - { - "filename": "labels.pkl", - "md5": "71069f78e83fa85dd6a4b9b6570447c2", - } - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - "train_mask": { - "upon": [ - { - "filename": "train_mask.pkl", - "md5": "3b831318fc3d3e588bead5ba469fe38f", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "val_mask": { - "upon": [ - { - "filename": "val_mask.pkl", - "md5": "c22eb5b7493908042c7e039c8bb5a82e", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "test_mask": { - "upon": [ - { - "filename": "test_mask.pkl", - "md5": "c22eb5b7493908042c7e039c8bb5a82e", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - }
- - -
[docs]class CocitationPubmed(BaseData): - r"""The Co-citation PubMed dataset is a citation network dataset for vertex classification task. - More details see the `HyperGCN <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper. - - The content of the Co-citation PubMed dataset includes the following: - - - ``num_classes``: The number of classes: :math:`3`. - - ``num_vertices``: The number of vertices: :math:`19,717`. - - ``num_edges``: The number of edges: :math:`7,963`. - - ``dim_features``: The dimension of features: :math:`500`. - - ``features``: The vertex feature matrix. ``torch.Tensor`` with size :math:`(19,717 \times 500)`. - - ``edge_list``: The edge list. ``List`` with length :math:`7,963`. - - ``labels``: The label list. ``torch.LongTensor`` with size :math:`(19,717, )`. - - ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(19,717, )`. - - ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(19,717, )`. - - ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(19,717, )`. - - Args: - ``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``. - """ - - def __init__(self, data_root: Optional[str] = None) -> None: - super().__init__("cocitation_pubmed", data_root) - self._content = { - "num_classes": 3, - "num_vertices": 19717, - "num_edges": 7963, - "dim_features": 500, - "features": { - "upon": [ - { - "filename": "features.pkl", - "md5": "f89502c432ca451156a8235c5efc034e", - } - ], - "loader": load_from_pickle, - "preprocess": [to_tensor, partial(norm_ft, ord=1)], - }, - "edge_list": { - "upon": [ - { - "filename": "edge_list.pkl", - "md5": "c5fbedf63e5be527f200e8c4e0391b00", - } - ], - "loader": load_from_pickle, - }, - "labels": { - "upon": [ - { - "filename": "labels.pkl", - "md5": "c039f778409a15f9b2ceefacad9c2202", - } - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - "train_mask": { - "upon": [ - { - "filename": "train_mask.pkl", - "md5": "81b422937f3adccd89a334d7093b67d7", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "val_mask": { - "upon": [ - { - "filename": "val_mask.pkl", - "md5": "10717940ddbfa3e4f6c0b148bb394f79", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "test_mask": { - "upon": [ - { - "filename": "test_mask.pkl", - "md5": "10717940ddbfa3e4f6c0b148bb394f79", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - }
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/contact_primary_school.html b/docs/_modules/easygraph/datasets/hypergraph/contact_primary_school.html deleted file mode 100644 index 5974b351..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/contact_primary_school.html +++ /dev/null @@ -1,296 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.contact_primary_school — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.contact_primary_school

-import requests
-
-from easygraph.utils.exception import EasyGraphError
-
-
-
[docs]def request_text_from_url(url): - """Requests text data from the specified URL. - - Args: - url (str): The URL from which to request data. - - Returns: - str: The text content of the response if the request is successful. - - Raises: - EasyGraphError: If a connection error occurs or the HTTP response status code indicates failure. - """ - try: - r = requests.get(url) - except requests.ConnectionError: - raise EasyGraphError("Connection Error!") - - if r.ok: - return r.text - else: - raise EasyGraphError(f"Error: HTTP response {r.status_code}")
- - -
[docs]class contact_primary_school: - """A class for loading and processing the primary school contact network hypergraph dataset. - - This class loads hyperedge, node label, and label name data from specified URLs and generates a hypergraph. - - Attributes: - data_root (str): The root URL for the data. If not provided, it is set to None. - hyperedges_path (str): The URL for the hyperedge data. - node_labels_path (str): The URL for the node label data. - label_names_path (str): The URL for the label name data. - _hyperedges (list): A list storing hyperedges. - _node_labels (list): A list storing node labels. - _label_names (list): A list storing label names. - _node_names (list): A list storing node names (currently unused). - _content (dict): A dictionary containing dataset statistics and data. - """ - - def __init__(self, data_root=None): - """Initializes an instance of the contact_primary_school class. - - Args: - data_root (str, optional): The root URL for the data. Defaults to None. - """ - self.data_root = "https://" if data_root is not None else data_root - self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-contact-primary-school/-/raw/main/hyperedges-contact-primary-school.txt?inline=false" - self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-contact-primary-school/-/raw/main/node-labels-contact-primary-school.txt?ref_type=heads&inline=false" - # self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false" - self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-contact-primary-school/-/raw/main/label-names-contact-primary-school.txt?ref_type=heads&inline=false" - self._hyperedges = [] - self._node_labels = [] - self._label_names = [] - self._node_names = [] - self.generate_hypergraph( - hyperedges_path=self.hyperedges_path, - node_labels_path=self.node_labels_path, - # node_names_path=self.node_names_path, - label_names_path=self.label_names_path, - ) - self._content = { - "num_classes": len(self._label_names), - "num_vertices": len(self._node_labels), - "num_edges": len(self._hyperedges), - "edge_list": self._hyperedges, - "labels": self._node_labels, - } - - def __getitem__(self, key: str): - """Accesses data in the _content dictionary by key. - - Args: - key (str): The key of the data to access. - - Returns: - Any: The value corresponding to the key in the _content dictionary. - """ - return self._content[key] - -
[docs] def process_label_txt(self, data_str, delimiter="\n", transform_fun=str): - """Processes label data read from a text file. - - Args: - data_str (str): A string containing label data. - delimiter (str, optional): The delimiter used to split the string. Defaults to "\n". - transform_fun (callable, optional): A function used to transform each label. Defaults to str. - - Returns: - list: A list of processed labels. - """ - data_str = data_str.strip() - data_lst = data_str.split(delimiter) - final_lst = [] - for data in data_lst: - data = data.strip() - data = transform_fun(data) - final_lst.append(data) - return final_lst
- - @property - def node_labels(self): - """Gets the list of node labels. - - Returns: - list: A list of node labels. - """ - return self._node_labels - - """ - @property - def node_names(self): - return self._node_names - """ - - @property - def label_names(self): - """Gets the list of label names. - - Returns: - list: A list of label names. - """ - return self._label_names - - @property - def hyperedges(self): - """Gets the list of hyperedges. - - Returns: - list: A list of hyperedges. - """ - return self._hyperedges - -
[docs] def generate_hypergraph( - self, - hyperedges_path=None, - node_labels_path=None, - # node_names_path=None, - label_names_path=None, - ): - """Generates hypergraph data from specified URLs. - - Args: - hyperedges_path (str, optional): The URL for the hyperedge data. Defaults to None. - node_labels_path (str, optional): The URL for the node label data. Defaults to None. - label_names_path (str, optional): The URL for the label name data. Defaults to None. - """ - - def fun(data): - """Converts the input data to an integer and subtracts 1. - - Args: - data (str): The input string data. - - Returns: - int: The converted integer data. - """ - data = int(data) - 1 - return data - - hyperedges_info = request_text_from_url(hyperedges_path) - hyperedges_info = hyperedges_info.strip() - hyperedges_lst = hyperedges_info.split("\n") - for hyperedge in hyperedges_lst: - hyperedge = hyperedge.strip() - hyperedge = [int(i) - 1 for i in hyperedge.split(",")] - self._hyperedges.append(tuple(hyperedge)) - # print(self.hyperedges) - - node_labels_info = request_text_from_url(node_labels_path) - - process_node_labels_info = self.process_label_txt( - node_labels_info, transform_fun=fun - ) - self._node_labels = process_node_labels_info - label_names_info = request_text_from_url(label_names_path) - process_label_names_info = self.process_label_txt(label_names_info) - self._label_names = process_label_names_info
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/cooking_200.html b/docs/_modules/easygraph/datasets/hypergraph/cooking_200.html deleted file mode 100644 index 5383d3d6..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/cooking_200.html +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.cooking_200 — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.cooking_200

-from typing import Optional
-
-from easygraph.datapipe import load_from_pickle
-from easygraph.datapipe import to_bool_tensor
-from easygraph.datapipe import to_long_tensor
-from easygraph.datasets.hypergraph.hypergraph_dataset_base import BaseData
-
-
-
[docs]class Cooking200(BaseData): - r"""The Cooking 200 dataset is collected from `Yummly.com <https://www.yummly.com/>`_ for vertex classification task. - It is a hypergraph dataset, in which vertex denotes the dish and hyperedge denotes - the ingredient. Each dish is also associated with category information, which indicates the dish's cuisine like - Chinese, Japanese, French, and Russian. - - The content of the Cooking200 dataset includes the following: - - - ``num_classes``: The number of classes: :math:`20`. - - ``num_vertices``: The number of vertices: :math:`7,403`. - - ``num_edges``: The number of edges: :math:`2,755`. - - ``edge_list``: The edge list. ``List`` with length :math:`(2,755)`. - - ``labels``: The label list. ``torch.LongTensor`` with size :math:`(7,403)`. - - ``train_mask``: The train mask. ``torch.BoolTensor`` with size :math:`(7,403)`. - - ``val_mask``: The validation mask. ``torch.BoolTensor`` with size :math:`(7,403)`. - - ``test_mask``: The test mask. ``torch.BoolTensor`` with size :math:`(7,403)`. - - Args: - ``data_root`` (``str``, optional): The ``data_root`` has stored the data. If set to ``None``, this function will auto-download from server and save into the default direction ``~/.dhg/datasets/``. Defaults to ``None``. - """ - - def __init__(self, data_root: Optional[str] = None) -> None: - super().__init__("cooking_200", data_root) - self._content = { - "num_classes": 20, - "num_vertices": 7403, - "num_edges": 2755, - "edge_list": { - "upon": [ - { - "filename": "edge_list.pkl", - "md5": "2cd32e13dd4e33576c43936542975220", - } - ], - "loader": load_from_pickle, - }, - "labels": { - "upon": [ - { - "filename": "labels.pkl", - "md5": "f1f3c0399c9c28547088f44e0bfd5c81", - } - ], - "loader": load_from_pickle, - "preprocess": [to_long_tensor], - }, - "train_mask": { - "upon": [ - { - "filename": "train_mask.pkl", - "md5": "66ea36bae024aaaed289e1998fe894bd", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "val_mask": { - "upon": [ - { - "filename": "val_mask.pkl", - "md5": "6c0d3d8b752e3955c64788cc65dcd018", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - "test_mask": { - "upon": [ - { - "filename": "test_mask.pkl", - "md5": "0e1564904551ba493e1f8a09d103461e", - } - ], - "loader": load_from_pickle, - "preprocess": [to_bool_tensor], - }, - }
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/hypergraph_dataset_base.html b/docs/_modules/easygraph/datasets/hypergraph/hypergraph_dataset_base.html deleted file mode 100644 index 63112d08..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/hypergraph_dataset_base.html +++ /dev/null @@ -1,232 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.hypergraph_dataset_base — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.hypergraph_dataset_base

-from pathlib import Path
-from typing import Any
-from typing import Dict
-from typing import List
-
-from easygraph.datapipe import compose_pipes
-from easygraph.datasets.hypergraph._global import DATASETS_ROOT
-from easygraph.datasets.hypergraph._global import REMOTE_DATASETS_ROOT
-from easygraph.datasets.utils import download_and_check
-
-
-
[docs]class BaseData: - r"""The Base Class of all datasets. - - :: - - self._content = { - 'item': { - 'upon': [ - {'filename': 'part1.pkl', 'md5': 'xxxxx',}, - {'filename': 'part2.pkl', 'md5': 'xxxxx',}, - ], - 'loader': loader_function, - 'preprocess': [datapipe1, datapipe2], - }, - ... - } - - """ - - def __init__(self, name: str, data_root=None): - # configure the data local/remote root - self.name = name - if data_root is None: - self.data_root = DATASETS_ROOT / name - else: - self.data_root = Path(data_root) / name - self.remote_root = REMOTE_DATASETS_ROOT + name + "/" - # init - self._content = {} - self._raw = {} - - def __repr__(self) -> str: - return ( - f"This is {self.name} dataset:\n" - + "\n".join(f" -> {k}" for k in self.content) - + "\nPlease try `data['name']` to get the specified data." - ) - - @property - def content(self): - r"""Return the content of the dataset.""" - return list(self._content.keys()) - -
[docs] def needs_to_load(self, item_name: str) -> bool: - r"""Return whether the ``item_name`` of the dataset needs to be loaded. - - Args: - ``item_name`` (``str``): The name of the item in the dataset. - """ - assert item_name in self.content, f"{item_name} is not provided in the Data" - return ( - isinstance(self._content[item_name], dict) - and "upon" in self._content[item_name] - and "loader" in self._content[item_name] - )
- - def __getitem__(self, key: str) -> Any: - if self.needs_to_load(key): - cur_cfg = self._content[key] - if cur_cfg.get("cache", None) is None: - # get raw data - item = self.raw(key) - # preprocess and cache - pipes = cur_cfg.get("preprocess", None) - if pipes is not None: - cur_cfg["cache"] = compose_pipes(*pipes)(item) - else: - cur_cfg["cache"] = item - return cur_cfg["cache"] - else: - return self._content[key] - -
[docs] def raw(self, key: str) -> Any: - r"""Return the ``key`` of the dataset with un-preprocessed format.""" - if self.needs_to_load(key): - cur_cfg = self._content[key] - if self._raw.get(key, None) is None: - upon = cur_cfg["upon"] - if len(upon) == 0: - return None - self.fetch_files(cur_cfg["upon"]) - file_path_list = [ - self.data_root / u["filename"] for u in cur_cfg["upon"] - ] - if len(file_path_list) == 1: - self._raw[key] = cur_cfg["loader"](file_path_list[0]) - else: - # here, you should implement a multi-file loader - self._raw[key] = cur_cfg["loader"](file_path_list) - return self._raw[key] - else: - return self._content[key]
- -
[docs] def fetch_files(self, files: List[Dict[str, str]]): - r"""Download and check the files if they are not exist. - - Args: - ``files`` (``List[Dict[str, str]]``): The files to download, each element - in the list is a dict with at lease two keys: ``filename`` and ``md5``. - If extra key ``bk_url`` is provided, it will be used to download the - file from the backup url. - """ - for file in files: - cur_filename = file["filename"] - cur_url = file.get("bk_url", None) - if cur_url is None: - cur_url = self.remote_root + cur_filename - download_and_check(cur_url, self.data_root / cur_filename, file["md5"])
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/mathoverflow_answers.html b/docs/_modules/easygraph/datasets/hypergraph/mathoverflow_answers.html deleted file mode 100644 index 27e1142a..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/mathoverflow_answers.html +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.mathoverflow_answers — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.mathoverflow_answers

-import requests
-
-from easygraph.utils.exception import EasyGraphError
-
-
-
[docs]def request_text_from_url(url): - try: - r = requests.get(url) - except requests.ConnectionError: - raise EasyGraphError("Connection Error!") - - if r.ok: - return r.text - else: - raise EasyGraphError(f"Error: HTTP response {r.status_code}")
- - -
[docs]class mathoverflow_answers: - def __init__(self, data_root=None): - self.data_root = "https://" if data_root is not None else data_root - self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-mathoverflow-answers/-/raw/main/hyperedges-mathoverflow-answers.txt?inline=false" - self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-mathoverflow-answers/-/raw/main/node-labels-mathoverflow-answers.txt?ref_type=heads&inline=false" - # self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-house-committees/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false" - self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-mathoverflow-answers/-/raw/main/label-names-mathoverflow-answers.txt?ref_type=heads&inline=false" - self._hyperedges = [] - self._node_labels = [] - self._label_names = [] - self._node_names = [] - self.generate_hypergraph( - hyperedges_path=self.hyperedges_path, - node_labels_path=self.node_labels_path, - # node_names_path=self.node_names_path, - label_names_path=self.label_names_path, - ) - self._content = { - "num_classes": len(self._label_names), - "num_vertices": len(self._node_labels), - "num_edges": len(self._hyperedges), - "edge_list": self._hyperedges, - "labels": self._node_labels, - } - - def __getitem__(self, key: str): - return self._content[key] - -
[docs] def process_label_txt(self, data_str, delimiter="\n", transform_fun=str): - data_str = data_str.strip() - data_lst = data_str.split(delimiter) - final_lst = [] - for data in data_lst: - data = data.strip() - data = transform_fun(data) - final_lst.append(data) - return final_lst
- - @property - def node_labels(self): - return self._node_labels - - """ - @property - def node_names(self): - return self._node_names - """ - - @property - def label_names(self): - return self._label_names - - @property - def hyperedges(self): - return self._hyperedges - -
[docs] def generate_hypergraph( - self, - hyperedges_path=None, - node_labels_path=None, - # node_names_path=None, - label_names_path=None, - ): - def fun(data): - data = int(data) - 1 - return data - - hyperedges_info = request_text_from_url(hyperedges_path) - hyperedges_info = hyperedges_info.strip() - hyperedges_lst = hyperedges_info.split("\n") - for hyperedge in hyperedges_lst: - hyperedge = hyperedge.strip() - hyperedge = [int(i) - 1 for i in hyperedge.split(",")] - self._hyperedges.append(tuple(hyperedge)) - # print(self.hyperedges) - """ - node_labels_info = request_text_from_url(node_labels_path) - - process_node_labels_info = self.process_label_txt( - node_labels_info, transform_fun=fun - ) - self._node_labels = process_node_labels_info - """ - node_labels_info = request_text_from_url(node_labels_path) - node_labels_info = node_labels_info.strip() - node_labels_lst = node_labels_info.split("\n") - for node_label in node_labels_lst: - node_label = node_label.strip() - node_label = [int(i) - 1 for i in node_label.split(",")] - self._node_labels.append(tuple(node_label)) - # print("process_node_labels_info:", process_node_labels_info) - # print("process_node_names_info:", process_node_names_info) - label_names_info = request_text_from_url(label_names_path) - process_label_names_info = self.process_label_txt(label_names_info) - self._label_names = process_label_names_info
- # print("process_label_names_info:", process_label_names_info) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/senate_committees.html b/docs/_modules/easygraph/datasets/hypergraph/senate_committees.html deleted file mode 100644 index d2e8b031..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/senate_committees.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.senate_committees — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.senate_committees

-import requests
-
-from easygraph.utils.exception import EasyGraphError
-
-
-
[docs]def request_text_from_url(url): - try: - r = requests.get(url) - except requests.ConnectionError: - raise EasyGraphError("Connection Error!") - - if r.ok: - return r.text - else: - raise EasyGraphError(f"Error: HTTP response {r.status_code}")
- - -
[docs]class senate_committees: - def __init__(self, data_root=None): - self.data_root = "https://" if data_root is not None else data_root - self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-senate-committees/-/raw/main/hyperedges-senate-committees.txt?inline=false" - self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-senate-committees/-/raw/main/node-labels-senate-committees.txt?ref_type=heads&inline=false" - self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-senate-committees/-/raw/main/node-names-senate-committees.txt?ref_type=heads&inline=false" - self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-senate-committees/-/raw/main/label-names-senate-committees.txt?ref_type=heads&inline=false" - self._hyperedges = [] - self._node_labels = [] - self._label_names = [] - self._node_names = [] - self.generate_hypergraph( - hyperedges_path=self.hyperedges_path, - node_labels_path=self.node_labels_path, - node_names_path=self.node_names_path, - label_names_path=self.label_names_path, - ) - self._content = { - "num_classes": len(self._label_names), - "num_vertices": len(self._node_labels), - "num_edges": len(self._hyperedges), - "edge_list": self._hyperedges, - "labels": self._node_labels, - } - - def __getitem__(self, key: str): - return self._content[key] - -
[docs] def process_label_txt(self, data_str, delimiter="\n", transform_fun=str): - data_str = data_str.strip() - data_lst = data_str.split(delimiter) - final_lst = [] - for data in data_lst: - data = data.strip() - data = transform_fun(data) - final_lst.append(data) - return final_lst
- - @property - def node_labels(self): - return self._node_labels - - @property - def node_names(self): - return self._node_names - - @property - def label_names(self): - return self._label_names - - @property - def hyperedges(self): - return self._hyperedges - -
[docs] def generate_hypergraph( - self, - hyperedges_path=None, - node_labels_path=None, - node_names_path=None, - label_names_path=None, - ): - def fun(data): - data = int(data) - 1 - return data - - hyperedges_info = request_text_from_url(hyperedges_path) - hyperedges_info = hyperedges_info.strip() - hyperedges_lst = hyperedges_info.split("\n") - for hyperedge in hyperedges_lst: - hyperedge = hyperedge.strip() - hyperedge = [int(i) - 1 for i in hyperedge.split(",")] - self._hyperedges.append(tuple(hyperedge)) - # print(self.hyperedges) - - node_labels_info = request_text_from_url(node_labels_path) - - process_node_labels_info = self.process_label_txt( - node_labels_info, transform_fun=fun - ) - self._node_labels = process_node_labels_info - # print("process_node_labels_info:", process_node_labels_info) - node_names_info = request_text_from_url(node_names_path) - process_node_names_info = self.process_label_txt(node_names_info) - self._node_names = process_node_names_info - # print("process_node_names_info:", process_node_names_info) - label_names_info = request_text_from_url(label_names_path) - process_label_names_info = self.process_label_txt(label_names_info) - self._label_names = process_label_names_info
- # print("process_label_names_info:", process_label_names_info) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/trivago_clicks.html b/docs/_modules/easygraph/datasets/hypergraph/trivago_clicks.html deleted file mode 100644 index f0acfb8f..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/trivago_clicks.html +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.trivago_clicks — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.trivago_clicks

-import requests
-
-from easygraph.utils.exception import EasyGraphError
-
-
-
[docs]def request_text_from_url(url): - try: - r = requests.get(url) - except requests.ConnectionError: - raise EasyGraphError("Connection Error!") - - if r.ok: - return r.text - else: - raise EasyGraphError(f"Error: HTTP response {r.status_code}")
- - -
[docs]class trivago_clicks: - def __init__(self, data_root=None): - self.data_root = "https://" if data_root is not None else data_root - self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-trivago-clicks/-/raw/main/hyperedges-trivago-clicks.txt?inline=false" - self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-trivago-clicks/-/raw/main/node-labels-trivago-clicks.txt?ref_type=heads&inline=false" - # self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-trivago-clicks/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false" - self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-trivago-clicks/-/raw/main/label-names-trivago-clicks.txt?ref_type=heads&inline=false" - self._hyperedges = [] - self._node_labels = [] - self._label_names = [] - self._node_names = [] - self.generate_hypergraph( - hyperedges_path=self.hyperedges_path, - node_labels_path=self.node_labels_path, - # node_names_path=self.node_names_path, - label_names_path=self.label_names_path, - ) - self._content = { - "num_classes": len(self._label_names), - "num_vertices": len(self._node_labels), - "num_edges": len(self._hyperedges), - "edge_list": self._hyperedges, - "labels": self._node_labels, - } - - def __getitem__(self, key: str): - return self._content[key] - -
[docs] def process_label_txt(self, data_str, delimiter="\n", transform_fun=str): - data_str = data_str.strip() - data_lst = data_str.split(delimiter) - final_lst = [] - for data in data_lst: - data = data.strip() - data = transform_fun(data) - final_lst.append(data) - return final_lst
- - @property - def node_labels(self): - return self._node_labels - - """ - @property - def node_names(self): - return self._node_names - """ - - @property - def label_names(self): - return self._label_names - - @property - def hyperedges(self): - return self._hyperedges - -
[docs] def generate_hypergraph( - self, - hyperedges_path=None, - node_labels_path=None, - # node_names_path=None, - label_names_path=None, - ): - def fun(data): - data = int(data) - 1 - return data - - hyperedges_info = request_text_from_url(hyperedges_path) - hyperedges_info = hyperedges_info.strip() - hyperedges_lst = hyperedges_info.split("\n") - for hyperedge in hyperedges_lst: - hyperedge = hyperedge.strip() - hyperedge = [int(i) - 1 for i in hyperedge.split(",")] - self._hyperedges.append(tuple(hyperedge)) - # print(self.hyperedges) - - node_labels_info = request_text_from_url(node_labels_path) - - process_node_labels_info = self.process_label_txt( - node_labels_info, transform_fun=fun - ) - self._node_labels = process_node_labels_info - # print("process_node_labels_info:", process_node_labels_info) - # print("process_node_names_info:", process_node_names_info) - label_names_info = request_text_from_url(label_names_path) - process_label_names_info = self.process_label_txt(label_names_info) - self._label_names = process_label_names_info
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/hypergraph/walmart_trips.html b/docs/_modules/easygraph/datasets/hypergraph/walmart_trips.html deleted file mode 100644 index e9db2e87..00000000 --- a/docs/_modules/easygraph/datasets/hypergraph/walmart_trips.html +++ /dev/null @@ -1,321 +0,0 @@ - - - - - - easygraph.datasets.hypergraph.walmart_trips — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.datasets.hypergraph.walmart_trips

-import requests
-
-from easygraph.utils.exception import EasyGraphError
-
-
-
[docs]def request_text_from_url(url): - """ - Requests text content from the given URL. - - Args: - url (str): The URL from which to request text data. - - Returns: - str: The text content of the response if the request is successful. - - Raises: - EasyGraphError: If a connection error occurs during the request or if the HTTP response status code is not OK. - """ - try: - r = requests.get(url) - except requests.ConnectionError: - raise EasyGraphError("Connection Error!") - - if r.ok: - return r.text - else: - raise EasyGraphError(f"Error: HTTP response {r.status_code}")
- - -
[docs]class walmart_trips: - """ - A class for loading and processing the Walmart trips hypergraph dataset. - - This class fetches hyperedge, node label, and label name data from predefined URLs, - processes the data, and generates a hypergraph representation. It also provides access - to various dataset attributes through properties and indexing. - - Attributes: - data_root (str): The root URL for the data. If provided during initialization, it is set to "https://"; - otherwise, it is None. - hyperedges_path (str): The URL of the file containing hyperedge information. - node_labels_path (str): The URL of the file containing node label information. - label_names_path (str): The URL of the file containing label name information. - _hyperedges (list): A list of tuples representing hyperedges. - _node_labels (list): A list of node labels. - _label_names (list): A list of label names. - _node_names (list): An empty list reserved for node names (currently unused). - _content (dict): A dictionary containing dataset statistics and data, such as the number of classes, - vertices, edges, the edge list, and node labels. - """ - - def __init__(self, data_root=None, local_path=None): - """ - Initializes an instance of the walmart_trips class. - - Args: - data_root (str, optional): The root URL for the data. If provided, it is set to "https://"; - otherwise, it is None. Defaults to None. - local_path (str, optional): Currently unused. Defaults to None. - """ - self.data_root = "https://" if data_root is not None else data_root - self.hyperedges_path = "https://gitlab.com/easy-graph/easygraph-data-walmart-trips/-/raw/main/hyperedges-walmart-trips.txt?inline=false" - self.node_labels_path = "https://gitlab.com/easy-graph/easygraph-data-walmart-trips/-/raw/main/node-labels-walmart-trips.txt?ref_type=heads&inline=false" - # self.node_names_path = "https://gitlab.com/easy-graph/easygraph-data-walmart-trips/-/raw/main/node-names-house-committees.txt?ref_type=heads&inline=false" - self.label_names_path = "https://gitlab.com/easy-graph/easygraph-data-walmart-trips/-/raw/main/label-names-walmart-trips.txt?ref_type=heads&inline=false" - self._hyperedges = [] - self._node_labels = [] - self._label_names = [] - self._node_names = [] - - self.generate_hypergraph( - hyperedges_path=self.hyperedges_path, - node_labels_path=self.node_labels_path, - # node_names_path=self.node_names_path, - label_names_path=self.label_names_path, - ) - - self._content = { - "num_classes": len(self._label_names), - "num_vertices": len(self._node_labels), - "num_edges": len(self._hyperedges), - "edge_list": self._hyperedges, - "labels": self._node_labels, - } - - def __getitem__(self, key: str): - """ - Retrieves a value from the _content dictionary using the specified key. - - Args: - key (str): The key used to access the _content dictionary. - - Returns: - Any: The value corresponding to the key in the _content dictionary. - """ - return self._content[key] - -
[docs] def process_label_txt(self, data_str, delimiter="\n", transform_fun=str): - """ - Processes a string containing label data into a list of transformed values. - - Args: - data_str (str): The input string containing label data. - delimiter (str, optional): The delimiter used to split the input string. Defaults to "\n". - transform_fun (callable, optional): A function used to transform each label value. - Defaults to the str function. - - Returns: - list: A list of transformed label values. - """ - data_str = data_str.strip() - data_lst = data_str.split(delimiter) - final_lst = [] - for data in data_lst: - data = data.strip() - data = transform_fun(data) - final_lst.append(data) - return final_lst
- - @property - def node_labels(self): - """ - Gets the list of node labels. - - Returns: - list: A list of node labels. - """ - return self._node_labels - - """ - @property - def node_names(self): - return self._node_names - """ - - @property - def label_names(self): - """ - Gets the list of label names. - - Returns: - list: A list of label names. - """ - return self._label_names - - @property - def hyperedges(self): - """ - Gets the list of hyperedges. - - Returns: - list: A list of tuples representing hyperedges. - """ - return self._hyperedges - -
[docs] def generate_hypergraph( - self, - hyperedges_path=None, - node_labels_path=None, - # node_names_path=None, - label_names_path=None, - ): - """ - Generates a hypergraph by fetching and processing data from the specified URLs. - - Args: - hyperedges_path (str, optional): The URL of the file containing hyperedge information. - Defaults to None. - node_labels_path (str, optional): The URL of the file containing node label information. - Defaults to None. - label_names_path (str, optional): The URL of the file containing label name information. - Defaults to None. - """ - - def fun(data): - """ - Converts a string to an integer and subtracts 1. - - Args: - data (str): The input string to be converted. - - Returns: - int: The converted integer value minus 1. - """ - data = int(data) - 1 - return data - - hyperedges_info = request_text_from_url(hyperedges_path) - hyperedges_info = hyperedges_info.strip() - hyperedges_lst = hyperedges_info.split("\n") - for hyperedge in hyperedges_lst: - hyperedge = hyperedge.strip() - hyperedge = [int(i) - 1 for i in hyperedge.split(",")] - self._hyperedges.append(tuple(hyperedge)) - # print(self.hyperedges) - - node_labels_info = request_text_from_url(node_labels_path) - - process_node_labels_info = self.process_label_txt( - node_labels_info, transform_fun=fun - ) - self._node_labels = process_node_labels_info - # print("process_node_labels_info:", process_node_labels_info) - # print("process_node_names_info:", process_node_names_info) - label_names_info = request_text_from_url(label_names_path) - process_label_names_info = self.process_label_txt(label_names_info) - self._label_names = process_label_names_info
- # print("process_label_names_info:", process_label_names_info) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/karate.html b/docs/_modules/easygraph/datasets/karate.html deleted file mode 100644 index 493a753a..00000000 --- a/docs/_modules/easygraph/datasets/karate.html +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - easygraph.datasets.karate — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.datasets.karate

-import easygraph as eg
-
-from .graph_dataset_base import EasyGraphDataset
-from .utils import _set_labels
-from .utils import tensor
-
-
-""" KarateClubDataset for inductive learning. """
-
-
-
[docs]class KarateClubDataset(EasyGraphDataset): - """Karate Club dataset for Node Classification - - Zachary's karate club is a social network of a university - karate club, described in the paper "An Information Flow - Model for Conflict and Fission in Small Groups" by Wayne W. Zachary. - The network became a popular example of community structure in - networks after its use by Michelle Girvan and Mark Newman in 2002. - Official website: `<http://konect.cc/networks/ucidata-zachary/>`_ - - Karate Club dataset statistics: - - - Nodes: 34 - - Edges: 156 - - Number of Classes: 2 - - Parameters - ---------- - transform : callable, optional - A transform that takes in a :class:`~eg.Graph` object and returns - a transformed version. The :class:`~eg.Graph` object will be - transformed before every access. - - Attributes - ---------- - num_classes : int - Number of node classes - - Examples - -------- - >>> dataset = KarateClubDataset() - >>> num_classes = dataset.num_classes - >>> g = dataset[0] - >>> labels = g.ndata['label'] - """ - - def __init__(self, transform=None): - super(KarateClubDataset, self).__init__(name="karate_club", transform=transform) - -
[docs] def process(self): - import numpy as np - - kc_graph = eg.get_graph_karateclub() - label = np.asarray( - [kc_graph.nodes[i]["club"] != "Mr. Hi" for i in kc_graph.nodes] - ).astype(np.int64) - label = tensor(label) - - kc_graph = _set_labels(kc_graph, label) - kc_graph.ndata["label"] = label - self._graph = kc_graph - self._data = [kc_graph]
- - @property - def num_classes(self): - """Number of classes.""" - return 2 - - def __getitem__(self, idx): - r"""Get graph object - - Parameters - ---------- - idx : int - Item index, KarateClubDataset has only one graph object - - Returns - ------- - :class:`eg.Graph` - - graph structure and labels. - - - ``ndata['label']``: ground truth labels - """ - assert idx == 0, "This dataset has only one graph" - if self._transform is None: - return self._graph - else: - return self._transform(self._graph) - - def __len__(self): - r"""The number of graphs in the dataset.""" - return 1
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/ppi.html b/docs/_modules/easygraph/datasets/ppi.html deleted file mode 100644 index d89b3f07..00000000 --- a/docs/_modules/easygraph/datasets/ppi.html +++ /dev/null @@ -1,329 +0,0 @@ - - - - - - easygraph.datasets.ppi — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.datasets.ppi

-""" PPIDataset for inductive learning. """
-import json
-import os
-
-import numpy as np
-
-from easygraph.classes.directed_graph import DiGraph
-
-from ..readwrite import json_graph
-from .graph_dataset_base import EasyGraphBuiltinDataset
-from .utils import _get_dgl_url
-from .utils import data_type_dict
-from .utils import tensor
-
-
-
[docs]class PPIDataset(EasyGraphBuiltinDataset): - r"""Protein-Protein Interaction dataset for inductive node classification - - A toy Protein-Protein Interaction network dataset. The dataset contains - 24 graphs. The average number of nodes per graph is 2372. Each node has - 50 features and 121 labels. 20 graphs for training, 2 for validation - and 2 for testing. - - Reference: `<http://snap.stanford.edu/graphsage/>`_ - - Statistics: - - - Train examples: 20 - - Valid examples: 2 - - Test examples: 2 - - Parameters - ---------- - mode : str - Must be one of ('train', 'valid', 'test'). - Default: 'train' - raw_dir : str - Raw file directory to download/contains the input data directory. - Default: ~/.eg/ - force_reload : bool - Whether to reload the dataset. - Default: False - verbose : bool - Whether to print out progress information. - Default: True. - transform : callable, optional - A transform that takes in a :class:`~eg.DGLGraph` object and returns - a transformed version. The :class:`~eg.DGLGraph` object will be - transformed before every access. - - Attributes - ---------- - num_labels : int - Number of labels for each node - labels : Tensor - Node labels - features : Tensor - Node features - - Examples - -------- - >>> dataset = PPIDataset(mode='valid') - >>> num_labels = dataset.num_labels - >>> for g in dataset: - .... feat = g.ndata['feat'] - .... label = g.ndata['label'] - .... # your code here - >>> - """ - - def __init__( - self, - mode="train", - raw_dir=None, - force_reload=False, - verbose=False, - transform=None, - ): - assert mode in ["train", "valid", "test"] - self.mode = mode - _url = _get_dgl_url("dataset/ppi.zip") - super(PPIDataset, self).__init__( - name="ppi", - url=_url, - raw_dir=raw_dir, - force_reload=force_reload, - verbose=verbose, - transform=transform, - ) - -
[docs] def process(self): - graph_file = os.path.join( - self.save_path, "ppi", "{}_graph.json".format(self.mode) - ) - label_file = os.path.join( - self.save_path, "ppi", "{}_labels.npy".format(self.mode) - ) - feat_file = os.path.join( - self.save_path, "ppi", "{}_feats.npy".format(self.mode) - ) - graph_id_file = os.path.join( - self.save_path, "ppi", "{}_graph_id.npy".format(self.mode) - ) - - g_data = json.load(open(graph_file)) - self._labels = np.load(label_file) - self._feats = np.load(feat_file) - self.graph = DiGraph(json_graph.node_link_graph(g_data)) - graph_id = np.load(graph_id_file) - - # lo, hi means the range of graph ids for different portion of the dataset, - # 20 graphs for training, 2 for validation and 2 for testing. - lo, hi = 1, 21 - if self.mode == "valid": - lo, hi = 21, 23 - elif self.mode == "test": - lo, hi = 23, 25 - - graph_masks = [] - self.graphs = [] - for g_id in range(lo, hi): - g_mask = np.where(graph_id == g_id)[0] - graph_masks.append(g_mask) - g = self.graph.nodes_subgraph(g_mask) - g.ndata["feat"] = tensor( - self._feats[g_mask], dtype=data_type_dict()["float32"] - ) - g.ndata["label"] = tensor( - self._labels[g_mask], dtype=data_type_dict()["float32"] - ) - self.graphs.append(g)
- -
[docs] def has_cache(self): - graph_list_path = os.path.join( - self.save_path, "{}_eg_graph_list.bin".format(self.mode) - ) - g_path = os.path.join(self.save_path, "{}_eg_graph.bin".format(self.mode)) - info_path = os.path.join(self.save_path, "{}_info.pkl".format(self.mode)) - return ( - os.path.exists(graph_list_path) - and os.path.exists(g_path) - and os.path.exists(info_path) - )
- -
[docs] def save(self): - graph_list_path = os.path.join( - self.save_path, "{}_eg_graph_list.bin".format(self.mode) - ) - g_path = os.path.join(self.save_path, "{}_eg_graph.bin".format(self.mode)) - info_path = os.path.join(self.save_path, "{}_info.pkl".format(self.mode))
- # save_graphs(graph_list_path, self.graphs) - # save_graphs(g_path, self.graph) - # save_info(info_path, {'labels': self._labels, 'feats': self._feats}) - - # def load(self): - # graph_list_path = os.path.join(self.save_path, '{}_eg_graph_list.bin'.format(self.mode)) - # g_path = os.path.join(self.save_path, '{}_eg_graph.bin'.format(self.mode)) - # info_path = os.path.join(self.save_path, '{}_info.pkl'.format(self.mode)) - # self.graphs = load_graphs(graph_list_path)[0] - # g, _ = load_graphs(g_path) - # self.graph = g[0] - # info = load_info(info_path) - # self._labels = info['labels'] - # self._feats = info['feats'] - - @property - def num_labels(self): - return 121 - - def __len__(self): - """Return number of samples in this dataset.""" - return len(self.graphs) - - def __getitem__(self, item): - """Get the item^th sample. - - Parameters - --------- - item : int - The sample index. - - Returns - ------- - :class:`eg.Graph` - graph structure, node features and node labels. - - - ``ndata['feat']``: node features - - ``ndata['label']``: node labels - """ - if self._transform is None: - return self.graphs[item] - else: - return self._transform(self.graphs[item])
- - -
[docs]class LegacyPPIDataset(PPIDataset): - """Legacy version of PPI Dataset""" - - def __getitem__(self, item): - """Get the item^th sample. - - Parameters - --------- - idx : int - The sample index. - - Returns - ------- - (eg.DGLGraph, Tensor, Tensor) - The graph, features and its label. - """ - if self._transform is None: - g = self.graphs[item] - else: - g = self._transform(self.graphs[item]) - return g, g.ndata["feat"], g.ndata["label"]
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/datasets/utils.html b/docs/_modules/easygraph/datasets/utils.html deleted file mode 100644 index f43d19b3..00000000 --- a/docs/_modules/easygraph/datasets/utils.html +++ /dev/null @@ -1,471 +0,0 @@ - - - - - - easygraph.datasets.utils — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.datasets.utils

-import errno
-import hashlib
-import numbers
-import os
-
-from pathlib import Path
-
-import numpy as np
-import requests
-import torch as th
-
-
-__all__ = [
-    "download",
-    "extract_archive",
-    "get_download_dir",
-    "makedirs",
-    "generate_mask_tensor",
-]
-
-import warnings
-
-from easygraph.utils.download import _retry
-
-
-def _get_eg_url(file_url):
-    """Get EasyGraph online url for download."""
-    eg_repo_url = "https://gitlab.com/easy-graph/"
-    repo_url = eg_repo_url
-    if repo_url[-1] != "/":
-        repo_url = repo_url + "/"
-    return repo_url + file_url
-
-
-def _get_dgl_url(file_url):
-    """Get DGL online url for download."""
-    dgl_repo_url = "https://data.dgl.ai/"
-    repo_url = os.environ.get("DGL_REPO", dgl_repo_url)
-    if repo_url[-1] != "/":
-        repo_url = repo_url + "/"
-    return repo_url + file_url
-
-
-def _set_labels(G, labels):
-    index = 0
-    for node in G.nodes:
-        G.add_node(node, label=labels[index])
-        index += 1
-    return G
-
-
-def _set_features(G, features):
-    index = 0
-    for node in G.nodes:
-        G.add_node(node, feat=features[index])
-        index += 1
-    return G
-
-
-def nonzero_1d(input):
-    x = th.nonzero(input, as_tuple=False).squeeze()
-    return x if x.dim() == 1 else x.view(-1)
-
-
-def tensor(data, dtype=None):
-    if isinstance(data, numbers.Number):
-        data = [data]
-    if isinstance(data, list) and len(data) > 0 and isinstance(data[0], th.Tensor):
-        # prevent GPU->CPU->GPU copies
-        if data[0].ndim == 0:
-            # zero dimension scalar tensors
-            return th.stack(data)
-    if isinstance(data, th.Tensor):
-        return th.as_tensor(data, dtype=dtype, device=data.device)
-    else:
-        return th.as_tensor(data, dtype=dtype)
-
-
-def data_type_dict():
-    return {
-        "float16": th.float16,
-        "float32": th.float32,
-        "float64": th.float64,
-        "uint8": th.uint8,
-        "int8": th.int8,
-        "int16": th.int16,
-        "int32": th.int32,
-        "int64": th.int64,
-        "bool": th.bool,
-    }
-
-
-
[docs]def download( - url, - path=None, - overwrite=True, - sha1_hash=None, - retries=5, - verify_ssl=True, - log=True, -): - """Download a given URL. - - Codes borrowed from mxnet/gluon/utils.py - - Parameters - ---------- - url : str - URL to download. - path : str, optional - Destination path to store downloaded file. By default stores to the - current directory with the same name as in url. - overwrite : bool, optional - Whether to overwrite the destination file if it already exists. - By default always overwrites the downloaded file. - sha1_hash : str, optional - Expected sha1 hash in hexadecimal digits. Will ignore existing file when hash is specified - but doesn't match. - retries : integer, default 5 - The number of times to attempt downloading in case of failure or non 200 return codes. - verify_ssl : bool, default True - Verify SSL certificates. - log : bool, default True - Whether to print the progress for download - - Returns - ------- - str - The file path of the downloaded file. - """ - if path is None: - fname = url.split("/")[-1] - # Empty filenames are invalid - assert fname, ( - "Can't construct file-name from this URL. " - "Please set the `path` option manually." - ) - else: - path = os.path.expanduser(path) - if os.path.isdir(path): - fname = os.path.join(path, url.split("/")[-1]) - else: - fname = path - assert retries >= 0, "Number of retries should be at least 0" - - if not verify_ssl: - warnings.warn( - "Unverified HTTPS request is being made (verify_ssl=False). " - "Adding certificate verification is strongly advised." - ) - - if ( - overwrite - or not os.path.exists(fname) - or (sha1_hash and not check_sha1(fname, sha1_hash)) - ): - dirname = os.path.dirname(os.path.abspath(os.path.expanduser(fname))) - if not os.path.exists(dirname): - os.makedirs(dirname) - while retries + 1 > 0: - # Disable pyling too broad Exception - # pylint: disable=W0703 - try: - if log: - print("Downloading %s from %s..." % (fname, url)) - r = requests.get(url, stream=True, verify=verify_ssl) - if r.status_code != 200: - raise RuntimeError("Failed downloading url %s" % url) - with open(fname, "wb") as f: - for chunk in r.iter_content(chunk_size=1024): - if chunk: # filter out keep-alive new chunks - f.write(chunk) - if sha1_hash and not check_sha1(fname, sha1_hash): - raise UserWarning( - "File {} is downloaded but the content hash does not match." - " The repo may be outdated or download may be incomplete. " - 'If the "repo_url" is overridden, consider switching to ' - "the default repo.".format(fname) - ) - break - except Exception as e: - retries -= 1 - if retries <= 0: - raise e - else: - if log: - print( - "download failed, retrying, {} attempt{} left".format( - retries, "s" if retries > 1 else "" - ) - ) - - return fname
- - -
[docs]def extract_archive(file, target_dir, overwrite=False): - """Extract archive file. - - Parameters - ---------- - file : str - Absolute path of the archive file. - target_dir : str - Target directory of the archive to be uncompressed. - overwrite : bool, default True - Whether to overwrite the contents inside the directory. - By default always overwrites. - """ - if os.path.exists(target_dir) and not overwrite: - return - print("Extracting file to {}".format(target_dir)) - if file.endswith(".tar.gz") or file.endswith(".tar") or file.endswith(".tgz"): - import tarfile - - with tarfile.open(file, "r") as archive: - archive.extractall(path=target_dir) - elif file.endswith(".gz"): - import gzip - import shutil - - with gzip.open(file, "rb") as f_in: - target_file = os.path.join(target_dir, os.path.basename(file)[:-3]) - with open(target_file, "wb") as f_out: - shutil.copyfileobj(f_in, f_out) - elif file.endswith(".zip"): - import zipfile - - with zipfile.ZipFile(file, "r") as archive: - archive.extractall(path=target_dir) - else: - raise Exception("Unrecognized file type: " + file)
- - -
[docs]def get_download_dir(): - """Get the absolute path to the download directory. - - Returns - ------- - dirname : str - Path to the download directory - """ - default_dir = os.path.join(os.path.expanduser("~"), ".EasyGraphData") - dirname = os.environ.get("EG_DOWNLOAD_DIR", default_dir) - if not os.path.exists(dirname): - os.makedirs(dirname) - return dirname
- - -
[docs]def makedirs(path): - try: - os.makedirs(os.path.expanduser(os.path.normpath(path))) - except OSError as e: - if e.errno != errno.EEXIST and os.path.isdir(path): - raise e
- - -def check_sha1(filename, sha1_hash): - """Check whether the sha1 hash of the file content matches the expected hash. - - Codes borrowed from mxnet/gluon/utils.py - - Parameters - ---------- - filename : str - Path to the file. - sha1_hash : str - Expected sha1 hash in hexadecimal digits. - - Returns - ------- - bool - Whether the file content matches the expected hash. - """ - sha1 = hashlib.sha1() - with open(filename, "rb") as f: - while True: - data = f.read(1048576) - if not data: - break - sha1.update(data) - - return sha1.hexdigest() == sha1_hash - - -
[docs]def generate_mask_tensor(mask): - """Generate mask tensor according to different backend - For torch, it will create a bool tensor - Parameters - ---------- - mask: numpy ndarray - input mask tensor - """ - assert isinstance( - mask, np.ndarray - ), "input for generate_mask_tensor should be an numpy ndarray" - return tensor(mask, dtype=data_type_dict()["bool"])
- - -def deprecate_property(old, new): - warnings.warn( - "Property {} will be deprecated, please use {} instead.".format(old, new) - ) - - -def check_file(file_path: Path, md5: str): - r"""Check if a file is valid. - - Args: - ``file_path`` (``Path``): The local path of the file. - ``md5`` (``str``): The md5 of the file. - - Raises: - FileNotFoundError: Not found the file. - """ - if not file_path.exists(): - raise FileNotFoundError(f"{file_path} does not exist.") - else: - with open(file_path, "rb") as f: - data = f.read() - cur_md5 = hashlib.md5(data).hexdigest() - return cur_md5 == md5 - - -def download_file(url: str, file_path: Path): - r"""Download a file from a url. - - Args: - ``url`` (``str``): the url of the file - ``file_path`` (``str``): the path to the file - """ - file_path.parent.mkdir(parents=True, exist_ok=True) - r = requests.get(url, stream=True, verify=True) - if r.status_code != 200: - raise requests.HTTPError(f"{url} is not accessible.") - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - - -@_retry(3) -def download_and_check(url: str, file_path: Path, md5: str): - r"""Download a file from a url and check its integrity. - - Args: - ``url`` (``str``): The url of the file. - ``file_path`` (``Path``): The path to the file. - ``md5`` (``str``): The md5 of the file. - """ - if not file_path.exists(): - download_file(url, file_path) - if not check_file(file_path, md5): - file_path.unlink() - raise ValueError( - f"{file_path} is corrupted. We will delete it, and try to download it" - " again." - ) - return True -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/exception.html b/docs/_modules/easygraph/exception.html deleted file mode 100644 index 584474c7..00000000 --- a/docs/_modules/easygraph/exception.html +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - easygraph.exception — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.exception

-"""
-**********
-Exceptions
-**********
-
-Base exceptions and errors for EasyGraph.
-"""
-
-__all__ = [
-    "HasACycle",
-    "NodeNotFound",
-    "EasyGraphAlgorithmError",
-    "EasyGraphException",
-    "EasyGraphError",
-    "EasyGraphNoCycle",
-    "EasyGraphNoPath",
-    "EasyGraphNotImplemented",
-    "EasyGraphPointlessConcept",
-    "EasyGraphUnbounded",
-    "EasyGraphUnfeasible",
-]
-
-
-
[docs]class EasyGraphException(Exception): - """Base class for exceptions in EasyGraph."""
- - -
[docs]class EasyGraphError(EasyGraphException): - """Exception for a serious error in EasyGraph"""
- - -
[docs]class EasyGraphPointlessConcept(EasyGraphException): - """Raised when a null graph is provided as input to an algorithm - that cannot use it. - - The null graph is sometimes considered a pointless concept [1]_, - thus the name of the exception. - - References - ---------- - .. [1] Harary, F. and Read, R. "Is the Null Graph a Pointless - Concept?" In Graphs and Combinatorics Conference, George - Washington University. New York: Springer-Verlag, 1973. - - """
- - -
[docs]class EasyGraphAlgorithmError(EasyGraphException): - """Exception for unexpected termination of algorithms."""
- - -
[docs]class EasyGraphUnfeasible(EasyGraphAlgorithmError): - """Exception raised by algorithms trying to solve a problem - instance that has no feasible solution."""
- - -
[docs]class EasyGraphNoPath(EasyGraphUnfeasible): - """Exception for algorithms that should return a path when running - on graphs where such a path does not exist."""
- - -
[docs]class EasyGraphNoCycle(EasyGraphUnfeasible): - """Exception for algorithms that should return a cycle when running - on graphs where such a cycle does not exist."""
- - -
[docs]class HasACycle(EasyGraphException): - """Raised if a graph has a cycle when an algorithm expects that it - will have no cycles. - - """
- - -
[docs]class EasyGraphUnbounded(EasyGraphAlgorithmError): - """Exception raised by algorithms trying to solve a maximization - or a minimization problem instance that is unbounded."""
- - -
[docs]class EasyGraphNotImplemented(EasyGraphException): - """Exception raised by algorithms not implemented for a type of graph."""
- - -
[docs]class NodeNotFound(EasyGraphException): - """Exception raised if requested node is not present in the graph"""
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/experiments/base.html b/docs/_modules/easygraph/experiments/base.html deleted file mode 100644 index e04a43bc..00000000 --- a/docs/_modules/easygraph/experiments/base.html +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - - - easygraph.experiments.base — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.experiments.base

-import abc
-import logging
-import shutil
-import time
-
-from copy import deepcopy
-from pathlib import Path
-from typing import Callable
-from typing import Optional
-from typing import Union
-
-import optuna
-import torch
-import torch.nn as nn
-
-from easygraph.classes.base import load_structure
-from easygraph.ml_metrics import BaseEvaluator
-from easygraph.utils import default_log_formatter
-from optuna.samplers import TPESampler
-
-
-
[docs]class BaseTask: - r"""The base class of Auto-experiment in EasyGraph. - - Args: - ``work_root`` (``Optional[Union[str, Path]]``): User's work root to store all studies. - ``data`` (``dict``): The dictionary to store input data that used in the experiment. - ``model_builder`` (``Callable``): The function to build a model with a fixed parameter ``trial``. - ``train_builder`` (``Callable``): The function to build a training configuration with two fixed parameters ``trial`` and ``model``. - ``evaluator`` (``eg.ml_metrics.BaseEvaluator``): The EasyGraph evaluator object to evaluate performance of the model in the experiment. - ``device`` (``torch.device``): The target device to run the experiment. - ``structure_builder`` (``Optional[Callable]``): The function to build a structure with a fixed parameter ``trial``. The structure can be ``eg.Graph``, ``eg.DiGraph``, ``eg.BiGraph``, and ``eg.Hypergraph``. - ``study_name`` (``Optional[str]``): The name of this study. If set to ``None``, the study name will be generated automatically according to current time. Defaults to ``None``. - ``overwrite`` (``bool``): The flag that whether to overwrite the existing study. Different studies are identified by the ``study_name``. Defaults to ``True``. - """ - - def __init__( - self, - work_root: Optional[Union[str, Path]], - data: dict, - model_builder: Callable, - train_builder: Callable, - evaluator: BaseEvaluator, - device: torch.device, - structure_builder: Optional[Callable] = None, - study_name: Optional[str] = None, - overwrite: bool = True, - ): - self.data = data - self.model_builder = model_builder - self.train_builder = train_builder - self.structure_builder = structure_builder - self.evaluator = evaluator - self.device = device - self.study = None - if study_name is None: - self.study_name = time.strftime("%Y-%m-%d--%H-%M-%S", time.localtime()) - else: - self.study_name = study_name - work_root = Path(work_root) - self.study_root = work_root / self.study_name - if overwrite and self.study_root.exists(): - shutil.rmtree(self.study_root) - self.log_file = self.study_root / "log.txt" - self.cache_root = self.study_root / "cache" - if not work_root.exists(): - if work_root.parent.exists(): - work_root.mkdir(exist_ok=True) - else: - raise ValueError(f"The work_root {work_root} does not exist.") - self.study_root.mkdir(exist_ok=True) - self.cache_root.mkdir(exist_ok=True) - # configure logging - self.logger = optuna.logging.get_logger("optuna") - self.logger.setLevel(logging.INFO) - out_file_handler = logging.FileHandler(self.log_file, mode="a", encoding="utf8") - out_file_handler.setFormatter(default_log_formatter()) - self.logger.addHandler(out_file_handler) - self.logger.info(f"Logs will be saved to {self.log_file.absolute()}") - self.logger.info( - f"Files in training will be saved in {self.study_root.absolute()}" - ) - -
[docs] def experiment(self, trial: optuna.Trial): - r"""Run the experiment for a given trial. - - Args: - ``trial`` (``optuna.Trial``): The ``optuna.Trial`` object. - """ - if self.structure_builder is not None: - self.data["structure"] = self.structure_builder(trial).to(self.device) - model = self.model_builder(trial).to(self.device) - train_configs: dict = self.train_builder(trial, model) - assert "optimizer" in train_configs.keys() - optimizer = train_configs["optimizer"] - assert "criterion" in train_configs.keys() - criterion = train_configs["criterion"] - scheduler = train_configs.get("scheduler", None) - - best_model = None - if self.direction == "maximize": - best_score = -float("inf") - else: - best_score = float("inf") - for epoch in range(self.max_epoch): - self.train(self.data, model, optimizer, criterion) - val_res = self.validate(self.data, model) - trial.report(val_res, epoch) - if trial.should_prune(): - raise optuna.exceptions.TrialPruned() - if scheduler is not None: - scheduler.step() - if self.direction == "maximize": - if val_res > best_score: - best_score = val_res - best_model = deepcopy(model) - with open(self.cache_root / f"{trial.number}_model.pth", "wb") as f: - torch.save(best_model.cpu().state_dict(), f) - self.data["structure"].save(self.cache_root / f"{trial.number}_structure.dhg") - return best_score
- - def _remove_cached_data(self): - r"""Remove cached models and structures.""" - if self.study is not None: - for filename in self.cache_root.glob("*"): - if filename.stem.split("_")[0] != str(self.study.best_trial.number): - filename.unlink() - -
[docs] def run(self, max_epoch: int, num_trials: int = 1, direction: str = "maximize"): - r"""Run experiments with automatically hyper-parameter tuning. - - Args: - ``max_epoch`` (``int``): The maximum number of epochs to train for each experiment. - ``num_trials`` (``int``): The number of trials to run. Defaults to ``1``. - ``direction`` (``str``): The direction to optimize. Defaults to ``"maximize"``. - """ - self.logger.info(f"Random seed is {dhg.random.seed()}") - sampler = TPESampler(seed=dhg.random.seed()) - self.max_epoch, self.direction = max_epoch, direction - self.study = optuna.create_study(direction=direction, sampler=sampler) - self.study.optimize(self.experiment, n_trials=num_trials, timeout=600) - - self._remove_cached_data() - self.best_model = self.model_builder(self.study.best_trial) - self.best_model.load_state_dict( - torch.load(f"{self.cache_root}/{self.study.best_trial.number}_model.pth") - ) - self.best_structure = load_structure( - f"{self.cache_root}/{self.study.best_trial.number}_structure.dhg" - ) - self.best_model = self.best_model.to(self.device) - self.best_structure = self.best_structure.to(self.device) - - self.logger.info("Best trial:") - self.best_trial = self.study.best_trial - self.logger.info(f"\tValue: {self.best_trial.value:.3f}") - self.logger.info(f"\tParams:") - for key, value in self.best_trial.params.items(): - self.logger.info(f"\t\t{key} |-> {value}") - test_res = self.test() - self.logger.info(f"Final test results:") - for key, value in test_res.items(): - self.logger.info(f"\t{key} |-> {value:.3f}")
- -
[docs] @abc.abstractmethod - def train( - self, - data: dict, - model: nn.Module, - optimizer: torch.optim.Optimizer, - criterion: nn.Module, - ): - r"""Train model for one epoch. - - Args: - ``data`` (``dict``): The input data. - ``model`` (``nn.Module``): The model. - ``optimizer`` (``torch.optim.Optimizer``): The model optimizer. - ``criterion`` (``nn.Module``): The loss function. - """
- -
[docs] @torch.no_grad() - @abc.abstractmethod - def validate( - self, - data: dict, - model: nn.Module, - ): - r"""Validate the model. - - Args: - ``data`` (``dict``): The input data. - ``model`` (``nn.Module``): The model. - """
- -
[docs] @torch.no_grad() - @abc.abstractmethod - def test(self, data: Optional[dict] = None, model: Optional[nn.Module] = None): - r"""Test the model. - - Args: - ``data`` (``dict``, optional): The input data if set to ``None``, the specified ``data`` in the initialization of the experiments will be used. Defaults to ``None``. - ``model`` (``nn.Module``, optional): The model if set to ``None``, the trained best model will be used. Defaults to ``None``. - """
-
- -
-
- -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/experiments/hypergraphs/hypergraph.html b/docs/_modules/easygraph/experiments/hypergraphs/hypergraph.html deleted file mode 100644 index 62760641..00000000 --- a/docs/_modules/easygraph/experiments/hypergraphs/hypergraph.html +++ /dev/null @@ -1,242 +0,0 @@ - - - - - - - - easygraph.experiments.hypergraphs.hypergraph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.experiments.hypergraphs.hypergraph

-from pathlib import Path
-from typing import Callable
-from typing import Optional
-from typing import Union
-
-import optuna
-import torch
-import torch.nn as nn
-
-from easygraph.ml_metrics import BaseEvaluator
-
-from ..vertex_classification import VertexClassificationTask
-
-
-
[docs]class HypergraphVertexClassificationTask(VertexClassificationTask): - r"""The auto-experiment class for the vertex classification task on hypergraph. - - Args: - ``work_root`` (``Optional[Union[str, Path]]``): User's work root to store all studies. - ``data`` (``dict``): The dictionary to store input data that used in the experiment. - ``model_builder`` (``Callable``): The function to build a model with a fixed parameter ``trial``. - ``train_builder`` (``Callable``): The function to build a training configuration with two fixed parameters ``trial`` and ``model``. - ``evaluator`` (``easygraph.ml_metrics.BaseEvaluator``): The DHG evaluator object to evaluate performance of the model in the experiment. - ``device`` (``torch.device``): The target device to run the experiment. - ``structure_builder`` (``Optional[Callable]``): The function to build a structure with a fixed parameter ``trial``. The structure should be ``easygraph.Hypergraph``. - ``study_name`` (``Optional[str]``): The name of this study. If set to ``None``, the study name will be generated automatically according to current time. Defaults to ``None``. - ``overwrite`` (``bool``): The flag that whether to overwrite the existing study. Different studies are identified by the ``study_name``. Defaults to ``True``. - """ - - def __init__( - self, - work_root: Optional[Union[str, Path]], - data: dict, - model_builder: Callable, - train_builder: Callable, - evaluator: BaseEvaluator, - device: torch.device, - structure_builder: Optional[Callable] = None, - study_name: Optional[str] = None, - overwrite: bool = True, - ): - super().__init__( - work_root, - data, - model_builder, - train_builder, - evaluator, - device, - structure_builder=structure_builder, - study_name=study_name, - overwrite=overwrite, - ) - -
[docs] def to(self, device: torch.device): - r"""Move the input data to the target device. - - Args: - ``device`` (``torch.device``): The specified target device to store the input data. - """ - return super().to(device)
- - @property - def vars_for_DL(self): - r"""Return a name list for available variables for deep learning in the vertex classification on hypergraph. The name list includes ``features``, ``structure``, ``labels``, ``train_mask``, ``val_mask``, and ``test_mask``. - """ - return super().vars_for_DL - -
[docs] def experiment(self, trial: optuna.Trial): - r"""Run the experiment for a given trial. - - Args: - ``trial`` (``optuna.Trial``): The ``optuna.Trial`` object. - """ - return super().experiment(trial)
- -
[docs] def run(self, max_epoch: int, num_trials: int = 1, direction: str = "maximize"): - r"""Run experiments with automatically hyper-parameter tuning. - - Args: - ``max_epoch`` (``int``): The maximum number of epochs to train for each experiment. - ``num_trials`` (``int``): The number of trials to run. Defaults to ``1``. - ``direction`` (``str``): The direction to optimize. Defaults to ``"maximize"``. - """ - return super().run(max_epoch, num_trials, direction)
- -
[docs] def train( - self, - data: dict, - model: nn.Module, - optimizer: torch.optim.Optimizer, - criterion: nn.Module, - ): - r"""Train model for one epoch. - - Args: - ``data`` (``dict``): The input data. - ``model`` (``nn.Module``): The model. - ``optimizer`` (``torch.optim.Optimizer``): The model optimizer. - ``criterion`` (``nn.Module``): The loss function. - """ - return super().train(data, model, optimizer, criterion)
- -
[docs] @torch.no_grad() - def validate(self, data: dict, model: nn.Module): - r"""Validate the model. - - Args: - ``data`` (``dict``): The input data. - ``model`` (``nn.Module``): The model. - """ - return super().validate(data, model)
- -
[docs] @torch.no_grad() - def test(self, data: Optional[dict] = None, model: Optional[nn.Module] = None): - r"""Test the model. - - Args: - ``data`` (``dict``, optional): The input data if set to ``None``, the specified ``data`` in the intialization of the experiments will be used. Defaults to ``None``. - ``model`` (``nn.Module``, optional): The model if set to ``None``, the trained best model will be used. Defaults to ``None``. - """ - return super().test(data, model)
-
- -
-
- -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/experiments/vertex_classification.html b/docs/_modules/easygraph/experiments/vertex_classification.html deleted file mode 100644 index cbe509f9..00000000 --- a/docs/_modules/easygraph/experiments/vertex_classification.html +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - easygraph.experiments.vertex_classification — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.experiments.vertex_classification

-from pathlib import Path
-from typing import Callable
-from typing import Optional
-from typing import Union
-
-import optuna
-import torch
-import torch.nn as nn
-
-from easygraph.ml_metrics import BaseEvaluator
-
-from .base import BaseTask
-
-
-
[docs]class VertexClassificationTask(BaseTask): - r"""The auto-experiment class for the vertex classification task. - - Args: - ``work_root`` (``Optional[Union[str, Path]]``): User's work root to store all studies. - ``data`` (``dict``): The dictionary to store input data that used in the experiment. - ``model_builder`` (``Callable``): The function to build a model with a fixed parameter ``trial``. - ``train_builder`` (``Callable``): The function to build a training configuration with two fixed parameters ``trial`` and ``model``. - ``evaluator`` (``eg.ml_metrics.BaseEvaluator``): The DHG evaluator object to evaluate performance of the model in the experiment. - ``device`` (``torch.device``): The target device to run the experiment. - ``structure_builder`` (``Optional[Callable]``): The function to build a structure with a fixed parameter ``trial``. The structure can be ``eg.Hypergraph``. - ``study_name`` (``Optional[str]``): The name of this study. If set to ``None``, the study name will be generated automatically according to current time. Defaults to ``None``. - ``overwrite`` (``bool``): The flag that whether to overwrite the existing study. Different studies are identified by the ``study_name``. Defaults to ``True``. - """ - - def __init__( - self, - work_root: Optional[Union[str, Path]], - data: dict, - model_builder: Callable, - train_builder: Callable, - evaluator: BaseEvaluator, - device: torch.device, - structure_builder: Optional[Callable] = None, - study_name: Optional[str] = None, - overwrite: bool = True, - ): - super().__init__( - work_root, - data, - model_builder, - train_builder, - evaluator, - device, - structure_builder=structure_builder, - study_name=study_name, - overwrite=overwrite, - ) - self.to(self.device) - -
[docs] def to(self, device: torch.device): - r"""Move the input data to the target device. - - Args: - ``device`` (``torch.device``): The specified target device to store the input data. - """ - self.device = device - for name in self.vars_for_DL: - if name in self.data.keys(): - self.data[name] = self.data[name].to(device) - return self
- - @property - def vars_for_DL(self): - r"""Return a name list for available variables for deep learning in the vertex classification task. The name list includes ``features``, ``structure``, ``labels``, ``train_mask``, ``val_mask``, and ``test_mask``. - """ - return ( - "features", - "structure", - "labels", - "train_mask", - "val_mask", - "test_mask", - ) - -
[docs] def experiment(self, trial: optuna.Trial): - r"""Run the experiment for a given trial. - - Args: - ``trial`` (``optuna.Trial``): The ``optuna.Trial`` object. - """ - return super().experiment(trial)
- -
[docs] def run(self, max_epoch: int, num_trials: int = 1, direction: str = "maximize"): - r"""Run experiments with automatically hyper-parameter tuning. - - Args: - ``max_epoch`` (``int``): The maximum number of epochs to train for each experiment. - ``num_trials`` (``int``): The number of trials to run. Defaults to ``1``. - ``direction`` (``str``): The direction to optimize. Defaults to ``"maximize"``. - """ - return super().run(max_epoch, num_trials, direction)
- -
[docs] def train( - self, - data: dict, - model: nn.Module, - optimizer: torch.optim.Optimizer, - criterion: nn.Module, - ): - r"""Train model for one epoch. - - Args: - ``data`` (``dict``): The input data. - ``model`` (``nn.Module``): The model. - ``optimizer`` (``torch.optim.Optimizer``): The model optimizer. - ``criterion`` (``nn.Module``): The loss function. - """ - features, structure = data["features"], data["structure"] - train_mask, labels = data["train_mask"], data["labels"] - model.train() - optimizer.zero_grad() - outputs = model(features, structure) - loss = criterion( - outputs[train_mask], - labels[train_mask], - ) - loss.backward() - optimizer.step()
- -
[docs] @torch.no_grad() - def validate(self, data: dict, model: nn.Module): - r"""Validate the model. - - Args: - ``data`` (``dict``): The input data. - ``model`` (``nn.Module``): The model. - """ - features, structure = data["features"], data["structure"] - val_mask, labels = data["val_mask"], data["labels"] - model.eval() - outputs = model(features, structure) - res = self.evaluator.validate(labels[val_mask], outputs[val_mask]) - return res
- -
[docs] @torch.no_grad() - def test(self, data: Optional[dict] = None, model: Optional[nn.Module] = None): - r"""Test the model. - - Args: - ``data`` (``dict``, optional): The input data if set to ``None``, the specified ``data`` in the initialization of the experiments will be used. Defaults to ``None``. - ``model`` (``nn.Module``, optional): The model if set to ``None``, the trained best model will be used. Defaults to ``None``. - """ - if data is None: - features, structure = self.data["features"], self.best_structure - test_mask, labels = self.data["test_mask"], self.data["labels"] - else: - features, structure = ( - data["features"].to(self.device), - data["structure"].to(self.device), - ) - test_mask, labels = ( - data["test_mask"].to(self.device), - data["labels"].to(self.device), - ) - if model is None: - model = self.best_model - model = model.to(self.device) - model.eval() - outputs = model(features, structure) - res = self.evaluator.test(labels[test_mask], outputs[test_mask]) - return res
-
- -
-
- -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/basic/avg_degree.html b/docs/_modules/easygraph/functions/basic/avg_degree.html deleted file mode 100644 index ab2efa3f..00000000 --- a/docs/_modules/easygraph/functions/basic/avg_degree.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - easygraph.functions.basic.avg_degree — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • »
  • -
  • Module code »
  • -
  • easygraph.functions.basic.avg_degree
  • -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.basic.avg_degree

-__all__ = [
-    "average_degree",
-]
-
-
-
[docs]def average_degree(G) -> float: - """Returns the average degree of the graph. - - Parameters - ---------- - G : graph - A EasyGraph graph - - Returns - ------- - average degree : float - The average degree of the graph. - - Notes - ----- - Self loops are counted twice in the total degree of a node. - - Examples - -------- - >>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc - >>> G.add_edge(1, 2) - >>> G.add_edge(2, 3) - >>> eg.average_degree(G) - 1.3333333333333333 - """ - return G.number_of_edges() / G.number_of_nodes() * 2
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/basic/cluster.html b/docs/_modules/easygraph/functions/basic/cluster.html deleted file mode 100644 index bf946394..00000000 --- a/docs/_modules/easygraph/functions/basic/cluster.html +++ /dev/null @@ -1,672 +0,0 @@ - - - - - - easygraph.functions.basic.cluster — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.basic.cluster

-from collections import Counter
-from itertools import chain
-
-import numpy as np
-
-from easygraph.utils.decorators import hybrid
-from easygraph.utils.decorators import not_implemented_for
-from easygraph.utils.misc import split
-from easygraph.utils.misc import split_len
-
-
-__all__ = ["average_clustering", "clustering"]
-
-
-def _local_weighted_triangles_and_degree_iter_parallel(
-    nodes_nbrs, G, weight, max_weight
-):
-    ret = []
-
-    def wt(u, v):
-        return G[u][v].get(weight, 1) / max_weight
-
-    for i, nbrs in nodes_nbrs:
-        inbrs = set(nbrs) - {i}
-        weighted_triangles = 0
-        seen = set()
-        for j in inbrs:
-            seen.add(j)
-            # This avoids counting twice -- we double at the end.
-            jnbrs = set(G[j]) - seen
-            # Only compute the edge weight once, before the inner inner
-            # loop.
-            wij = wt(i, j)
-            weighted_triangles += sum(
-                np.cbrt([(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs])
-            )
-        ret.append((i, len(inbrs), 2 * weighted_triangles))
-    return ret
-
-
-@not_implemented_for("multigraph")
-def _weighted_triangles_and_degree_iter(G, nodes=None, weight="weight", n_workers=None):
-    """Return an iterator of (node, degree, weighted_triangles).
-
-    Used for weighted clustering.
-    Note: this returns the geometric average weight of edges in the triangle.
-    Also, each triangle is counted twice (each direction).
-    So you may want to divide by 2.
-
-    """
-
-    if weight is None or G.number_of_edges() == 0:
-        max_weight = 1
-    else:
-        max_weight = max(d.get(weight, 1) for u, v, d in G.edges)
-    if nodes is None:
-        nodes_nbrs = G.adj.items()
-    else:
-        nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
-
-    def wt(u, v):
-        return G[u][v].get(weight, 1) / max_weight
-
-    if n_workers is not None:
-        import random
-
-        from functools import partial
-        from multiprocessing import Pool
-
-        _local_weighted_triangles_and_degree_iter_function = partial(
-            _local_weighted_triangles_and_degree_iter_parallel,
-            G=G,
-            weight=weight,
-            max_weight=max_weight,
-        )
-        nodes_nbrs = list(nodes_nbrs)
-        random.shuffle(nodes_nbrs)
-        if len(nodes_nbrs) > n_workers * 30000:
-            nodes_nbrs = split_len(nodes, step=30000)
-        else:
-            nodes_nbrs = split(nodes_nbrs, n_workers)
-        with Pool(n_workers) as p:
-            ret = p.imap(_local_weighted_triangles_and_degree_iter_function, nodes_nbrs)
-            for r in ret:
-                for x in r:
-                    yield x
-    else:
-        for i, nbrs in nodes_nbrs:
-            inbrs = set(nbrs) - {i}
-            weighted_triangles = 0
-            seen = set()
-            for j in inbrs:
-                seen.add(j)
-                # This avoids counting twice -- we double at the end.
-                jnbrs = set(G[j]) - seen
-                # Only compute the edge weight once, before the inner inner
-                # loop.
-                wij = wt(i, j)
-                weighted_triangles += sum(
-                    np.cbrt([(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs])
-                )
-            yield (i, len(inbrs), 2 * weighted_triangles)
-
-
-def _local_directed_weighted_triangles_and_degree_parallel(
-    nodes_nbrs, G, weight, max_weight
-):
-    ret = []
-
-    def wt(u, v):
-        return G[u][v].get(weight, 1) / max_weight
-
-    for i, preds, succs in nodes_nbrs:
-        ipreds = set(preds) - {i}
-        isuccs = set(succs) - {i}
-
-        directed_triangles = 0
-        for j in ipreds:
-            jpreds = set(G._pred[j]) - {j}
-            jsuccs = set(G._adj[j]) - {j}
-            directed_triangles += sum(
-                np.cbrt([(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
-            )
-            directed_triangles += sum(
-                np.cbrt([(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
-            )
-            directed_triangles += sum(
-                np.cbrt([(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
-            )
-            directed_triangles += sum(
-                np.cbrt([(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
-            )
-
-        for j in isuccs:
-            jpreds = set(G._pred[j]) - {j}
-            jsuccs = set(G._adj[j]) - {j}
-            directed_triangles += sum(
-                np.cbrt([(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
-            )
-            directed_triangles += sum(
-                np.cbrt([(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
-            )
-            directed_triangles += sum(
-                np.cbrt([(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
-            )
-            directed_triangles += sum(
-                np.cbrt([(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
-            )
-
-        dtotal = len(ipreds) + len(isuccs)
-        dbidirectional = len(ipreds & isuccs)
-        ret.append([i, dtotal, dbidirectional, directed_triangles])
-    return ret
-
-
-@not_implemented_for("multigraph")
-def _directed_weighted_triangles_and_degree_iter(
-    G, nodes=None, weight="weight", n_workers=None
-):
-    """Return an iterator of
-    (node, total_degree, reciprocal_degree, directed_weighted_triangles).
-
-    Used for directed weighted clustering.
-    Note that unlike `_weighted_triangles_and_degree_iter()`, this function counts
-    directed triangles so does not count triangles twice.
-
-    """
-
-    if weight is None or G.number_of_edges() == 0:
-        max_weight = 1
-    else:
-        max_weight = max(d.get(weight, 1) for u, v, d in G.edges)
-
-    nodes_nbrs = ((n, G._pred[n], G._adj[n]) for n in G.nbunch_iter(nodes))
-
-    def wt(u, v):
-        return G[u][v].get(weight, 1) / max_weight
-
-    if n_workers is not None:
-        import random
-
-        from functools import partial
-        from multiprocessing import Pool
-
-        _local_directed_weighted_triangles_and_degree_function = partial(
-            _local_directed_weighted_triangles_and_degree_parallel,
-            G=G,
-            weight=weight,
-            max_weight=max_weight,
-        )
-        nodes_nbrs = list(nodes_nbrs)
-        random.shuffle(nodes_nbrs)
-        if len(nodes_nbrs) > n_workers * 30000:
-            nodes_nbrs = split_len(nodes, step=30000)
-        else:
-            nodes_nbrs = split(nodes_nbrs, n_workers)
-        with Pool(n_workers) as p:
-            ret = p.imap(
-                _local_directed_weighted_triangles_and_degree_function, nodes_nbrs
-            )
-            for r in ret:
-                for x in r:
-                    yield x
-
-    else:
-        for i, preds, succs in nodes_nbrs:
-            ipreds = set(preds) - {i}
-            isuccs = set(succs) - {i}
-
-            directed_triangles = 0
-            for j in ipreds:
-                jpreds = set(G._pred[j]) - {j}
-                jsuccs = set(G._adj[j]) - {j}
-                directed_triangles += sum(
-                    np.cbrt([(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
-                )
-                directed_triangles += sum(
-                    np.cbrt([(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
-                )
-                directed_triangles += sum(
-                    np.cbrt([(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
-                )
-                directed_triangles += sum(
-                    np.cbrt([(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
-                )
-
-            for j in isuccs:
-                jpreds = set(G._pred[j]) - {j}
-                jsuccs = set(G._adj[j]) - {j}
-                directed_triangles += sum(
-                    np.cbrt([(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
-                )
-                directed_triangles += sum(
-                    np.cbrt([(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
-                )
-                directed_triangles += sum(
-                    np.cbrt([(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
-                )
-                directed_triangles += sum(
-                    np.cbrt([(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
-                )
-
-            dtotal = len(ipreds) + len(isuccs)
-            dbidirectional = len(ipreds & isuccs)
-            yield (i, dtotal, dbidirectional, directed_triangles)
-
-
-
[docs]def average_clustering(G, nodes=None, weight=None, count_zeros=True, n_workers=None): - r"""Compute the average clustering coefficient for the graph G. - - The clustering coefficient for the graph is the average, - - .. math:: - - C = \frac{1}{n}\sum_{v \in G} c_v, - - where :math:`n` is the number of nodes in `G`. - - Parameters - ---------- - G : graph - - nodes : container of nodes, optional (default=all nodes in G) - Compute average clustering for nodes in this container. - - weight : string or None, optional (default=None) - The edge attribute that holds the numerical value used as a weight. - If None, then each edge has weight 1. - - count_zeros : bool - If False include only the nodes with nonzero clustering in the average. - - Returns - ------- - avg : float - Average clustering - - Examples - -------- - >>> G = eg.complete_graph(5) - >>> print(eg.average_clustering(G)) - 1.0 - - Notes - ----- - This is a space saving routine; it might be faster - to use the clustering function to get a list and then take the average. - - Self loops are ignored. - - References - ---------- - .. [1] Generalizations of the clustering coefficient to weighted - complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela, - K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007). - http://jponnela.com/web_documents/a9.pdf - .. [2] Marcus Kaiser, Mean clustering coefficients: the role of isolated - nodes and leafs on clustering measures for small-world networks. - https://arxiv.org/abs/0802.2512 - """ - c = clustering(G, nodes, weight=weight, n_workers=n_workers).values() - if not count_zeros: - c = [v for v in c if abs(v) > 0] - return sum(c) / len(c)
- - -def _local_directed_triangles_and_degree_iter_parallel(nodes_nbrs, G): - ret = [] - for i, preds, succs in nodes_nbrs: - ipreds = set(preds) - {i} - isuccs = set(succs) - {i} - - directed_triangles = 0 - for j in chain(ipreds, isuccs): - jpreds = set(G._pred[j]) - {j} - jsuccs = set(G._adj[j]) - {j} - directed_triangles += sum( - 1 - for k in chain( - (ipreds & jpreds), - (ipreds & jsuccs), - (isuccs & jpreds), - (isuccs & jsuccs), - ) - ) - dtotal = len(ipreds) + len(isuccs) - dbidirectional = len(ipreds & isuccs) - ret.append((i, dtotal, dbidirectional, directed_triangles)) - return ret - - -@not_implemented_for("multigraph") -def _directed_triangles_and_degree_iter(G, nodes=None, n_workers=None): - """Return an iterator of - (node, total_degree, reciprocal_degree, directed_triangles). - - Used for directed clustering. - Note that unlike `_triangles_and_degree_iter()`, this function counts - directed triangles so does not count triangles twice. - - """ - nodes_nbrs = ((n, G._pred[n], G._adj[n]) for n in G.nbunch_iter(nodes)) - - if n_workers is not None: - import random - - from functools import partial - from multiprocessing import Pool - - _local_directed_triangles_and_degree_iter_parallel_function = partial( - _local_directed_triangles_and_degree_iter_parallel, G=G - ) - nodes_nbrs = list(nodes_nbrs) - random.shuffle(nodes_nbrs) - if len(nodes_nbrs) > n_workers * 30000: - nodes_nbrs = split_len(nodes_nbrs, step=30000) - else: - nodes_nbrs = split(nodes_nbrs, n_workers) - - with Pool(n_workers) as p: - ret = p.imap( - _local_directed_triangles_and_degree_iter_parallel_function, nodes_nbrs - ) - for r in ret: - for x in r: - yield x - else: - for i, preds, succs in nodes_nbrs: - ipreds = set(preds) - {i} - isuccs = set(succs) - {i} - - directed_triangles = 0 - for j in chain(ipreds, isuccs): - jpreds = set(G._pred[j]) - {j} - jsuccs = set(G._adj[j]) - {j} - directed_triangles += sum( - 1 - for k in chain( - (ipreds & jpreds), - (ipreds & jsuccs), - (isuccs & jpreds), - (isuccs & jsuccs), - ) - ) - dtotal = len(ipreds) + len(isuccs) - dbidirectional = len(ipreds & isuccs) - yield (i, dtotal, dbidirectional, directed_triangles) - - -def _local_triangles_and_degree_iter_function_parallel(nodes_nbrs, G): - ret = [] - for v, v_nbrs in nodes_nbrs: - vs = set(v_nbrs) - {v} - gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs) - ntriangles = sum(k * val for k, val in gen_degree.items()) - ret.append((v, len(vs), ntriangles, gen_degree)) - return ret - - -@not_implemented_for("multigraph") -def _triangles_and_degree_iter(G, nodes=None, n_workers=None): - """Return an iterator of (node, degree, triangles, generalized degree). - - This double counts triangles so you may want to divide by 2. - See degree(), triangles() and generalized_degree() for definitions - and details. - - """ - if nodes is None: - nodes_nbrs = G.adj.items() - else: - nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes)) - - if n_workers is not None: - import random - - from functools import partial - from multiprocessing import Pool - - _local_triangles_and_degree_iter_function = partial( - _local_triangles_and_degree_iter_function_parallel, G=G - ) - nodes_nbrs = list(nodes_nbrs) - random.shuffle(nodes_nbrs) - if len(nodes_nbrs) > n_workers * 30000: - nodes_nbrs = split_len(nodes_nbrs, step=30000) - else: - nodes_nbrs = split(nodes_nbrs, n_workers) - - with Pool(n_workers) as p: - ret = p.imap(_local_triangles_and_degree_iter_function, nodes_nbrs) - for r in ret: - for x in r: - yield x - else: - for v, v_nbrs in nodes_nbrs: - vs = set(v_nbrs) - {v} - gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs) - ntriangles = sum(k * val for k, val in gen_degree.items()) - yield (v, len(vs), ntriangles, gen_degree) - - -
[docs]@hybrid("cpp_clustering") -def clustering(G, nodes=None, weight=None, n_workers=None): - r"""Compute the clustering coefficient for nodes. - - For unweighted graphs, the clustering of a node :math:`u` - is the fraction of possible triangles through that node that exist, - - .. math:: - - c_u = \frac{2 T(u)}{deg(u)(deg(u)-1)}, - - where :math:`T(u)` is the number of triangles through node :math:`u` and - :math:`deg(u)` is the degree of :math:`u`. - - For weighted graphs, there are several ways to define clustering [1]_. - the one used here is defined - as the geometric average of the subgraph edge weights [2]_, - - .. math:: - - c_u = \frac{1}{deg(u)(deg(u)-1))} - \sum_{vw} (\hat{w}_{uv} \hat{w}_{uw} \hat{w}_{vw})^{1/3}. - - The edge weights :math:`\hat{w}_{uv}` are normalized by the maximum weight - in the network :math:`\hat{w}_{uv} = w_{uv}/\max(w)`. - - The value of :math:`c_u` is assigned to 0 if :math:`deg(u) < 2`. - - Additionally, this weighted definition has been generalized to support negative edge weights [3]_. - - For directed graphs, the clustering is similarly defined as the fraction - of all possible directed triangles or geometric average of the subgraph - edge weights for unweighted and weighted directed graph respectively [4]_. - - .. math:: - - c_u = \frac{2}{deg^{tot}(u)(deg^{tot}(u)-1) - 2deg^{\leftrightarrow}(u)} - T(u), - - where :math:`T(u)` is the number of directed triangles through node - :math:`u`, :math:`deg^{tot}(u)` is the sum of in degree and out degree of - :math:`u` and :math:`deg^{\leftrightarrow}(u)` is the reciprocal degree of - :math:`u`. - - - Parameters - ---------- - G : graph - - nodes : container of nodes, optional (default=all nodes in G) - Compute clustering for nodes in this container. - - weight : string or None, optional (default=None) - The edge attribute that holds the numerical value used as a weight. - If None, then each edge has weight 1. - - Returns - ------- - out : float, or dictionary - Clustering coefficient at specified nodes - - Examples - -------- - >>> G = eg.complete_graph(5) - >>> print(eg.clustering(G, 0)) - 1.0 - >>> print(eg.clustering(G)) - {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0} - - Notes - ----- - Self loops are ignored. - - References - ---------- - .. [1] Generalizations of the clustering coefficient to weighted - complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela, - K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007). - http://jponnela.com/web_documents/a9.pdf - .. [2] Intensity and coherence of motifs in weighted complex - networks by J. P. Onnela, J. Saramäki, J. Kertész, and K. Kaski, - Physical Review E, 71(6), 065103 (2005). - .. [3] Generalization of Clustering Coefficients to Signed Correlation Networks - by G. Costantini and M. Perugini, PloS one, 9(2), e88669 (2014). - .. [4] Clustering in complex directed networks by G. Fagiolo, - Physical Review E, 76(2), 026107 (2007). - """ - - if G.is_directed(): - if weight is not None: - td_iter = _directed_weighted_triangles_and_degree_iter( - G, nodes, weight, n_workers=n_workers - ) - clusterc = { - v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2) - for v, dt, db, t in td_iter - } - else: - td_iter = _directed_triangles_and_degree_iter(G, nodes, n_workers=n_workers) - clusterc = { - v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2) - for v, dt, db, t in td_iter - } - else: - # The formula 2*T/(d*(d-1)) from docs is t/(d*(d-1)) here b/c t==2*T - if weight is not None: - td_iter = _weighted_triangles_and_degree_iter( - G, nodes, weight, n_workers=n_workers - ) - clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t in td_iter} - else: - td_iter = _triangles_and_degree_iter(G, nodes, n_workers=n_workers) - clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t, _ in td_iter} - if nodes in G: - # Return the value of the sole entry in the dictionary. - return clusterc[nodes] - return clusterc
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/basic/localassort.html b/docs/_modules/easygraph/functions/basic/localassort.html deleted file mode 100644 index 05cb694b..00000000 --- a/docs/_modules/easygraph/functions/basic/localassort.html +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - easygraph.functions.basic.localassort — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.basic.localassort

-import easygraph as eg
-import numpy as np
-import scipy.sparse as sparse
-
-
-__all__ = [
-    "localAssort",
-]
-
-
-
[docs]def localAssort( - edgelist, node_attr, pr=np.arange(0.0, 1.0, 0.1), undir=True, missingValue=-1 -): - """Calculate the multiscale assortativity. - You must ensure that the node index and node attribute index start from 0 - Parameters - ---------- - edgelist : array_like - the network represented as an edge list, - i.e., a E x 2 array of node pairs - node_attr : array_like - n length array of node attribute values - pr : array, optional - array of one minus restart probabilities for the random walk in - calculating the personalised pagerank. The largest of these values - determines the accuracy of the TotalRank vector max(pr) -> 1 is more - accurate (default: [0, .1, .2, .3, .4, .5, .6, .7, .8, .9]) - undir : bool, optional - indicate if network is undirected (default: True) - missingValue : int, optional - token to indicate missing attribute values (default: -1) - Returns - ------- - assortM : array_like - n x len(pr) array of local assortativities, each column corresponds to - a value of the input restart probabilities, pr. Note if only number of - restart probabilties is greater than one (i.e., len(pr) > 1). - assortT : array_like - n length array of multiscale assortativities - Z : array_like - N length array of per-node confidence scores - References - ---------- - For full details see [1]_ - .. [1] Peel, L., Delvenne, J. C., & Lambiotte, R. (2018). "Multiscale - mixing patterns in networks.' PNAS, 115(16), 4057-4062. - """ - # number of nodes - n = len(node_attr) - - # number of nodes with complete attribute - ncomp = (node_attr != missingValue).sum() - # number of edges - m = len(edgelist) - # construct adjacency matrix and calculate degree sequence - A, degree = createA(edgelist, n, undir) - - # construct diagonal inverse degree matrix - D = sparse.diags(1.0 / degree, 0, format="csc") - - # construct transition matrix (row normalised adjacency matrix) - W = D @ A - - # number of distinct node categories - c = len(np.unique(node_attr)) - if ncomp < n: - c -= 1 - - # calculate node weights for how "complete" the - # metadata is around the node - Z = np.zeros(n) - - Z[node_attr == missingValue] = 1.0 - - Z = (W @ Z) / degree - - # indicator array if node has attribute data (or missing) - hasAttribute = node_attr != missingValue - - # calculate global expected values - values = np.ones(ncomp) - - yi = (hasAttribute).nonzero()[0] - - yj = node_attr[hasAttribute] - Y = sparse.coo_matrix((values, (yi, yj)), shape=(n, c)).tocsc() - eij_glob = np.array(Y.T @ (A @ Y).todense()) - - eij_glob /= np.sum(eij_glob) - - ab_glob = np.sum(eij_glob.sum(1) * eij_glob.sum(0)) - # initialise outputs - assortM = np.empty((n, len(pr))) - assortT = np.empty(n) - WY = (W @ Y).tocsc() - - for i in range(n): - pis, ti, it = calculateRWRrange(W, i, pr, n) - if len(pr) > 1: - for ii, pri in enumerate(pr): - pi = pis[:, ii] - - YPI = sparse.coo_matrix( - ( - pi[hasAttribute], - (node_attr[hasAttribute], np.arange(n)[hasAttribute]), - ), - shape=(c, n), - ).tocsr() - trace_e = (YPI.dot(WY).toarray()).trace() - assortM[i, ii] = trace_e - YPI = sparse.coo_matrix( - (ti[hasAttribute], (node_attr[hasAttribute], np.arange(n)[hasAttribute])), - shape=(c, n), - ).tocsr() - e_gh = (YPI @ WY).toarray() - e_gh_sum = e_gh.sum() - Z[i] = e_gh_sum - e_gh /= e_gh_sum - trace_e = e_gh.trace() - assortT[i] = trace_e - - assortT -= ab_glob - np.divide(assortT, 1.0 - ab_glob, out=assortT, where=ab_glob != 0) - - if len(pr) > 1: - assortM -= ab_glob - np.divide(assortM, 1.0 - ab_glob, out=assortM, where=ab_glob != 0) - return assortM, assortT, Z - return None, assortT, Z
- - -def createA(E, n, undir=True): - """Create adjacency matrix and degree sequence.""" - if undir: - G = eg.Graph() - else: - G = eg.DiGraph() - G.add_nodes_from(range(n)) - - for e in E: - G.add_edge(e[0], e[1]) - - A = eg.to_scipy_sparse_matrix(G) - - degree = np.array(A.sum(1)).flatten() - - return A, degree - - -def calculateRWRrange(W, i, alphas, n, maxIter=1000): - """ - Calculate the personalised TotalRank and personalised PageRank vectors. - Parameters - ---------- - W : array_like - transition matrix (row normalised adjacency matrix) - i : int - index of the personalisation node - alphas : array_like - array of (1 - restart probabilties) - n : int - number of nodes in the network - maxIter : int, optional - maximum number of interations (default: 1000) - Returns - ------- - pPageRank_all : array_like - personalised PageRank for all input alpha values (only calculated if - more than one alpha given as input, i.e., len(alphas) > 1) - pTotalRank : array_like - personalised TotalRank (personalised PageRank with alpha integrated - out) - - it : int - number of iterations - References - ---------- - See [2]_ and [3]_ for further details. - .. [2] Boldi, P. (2005). "TotalRank: Ranking without damping." In Special - interest tracks and posters of the 14th international conference on - World Wide Web (pp. 898-899). - .. [3] Boldi, P., Santini, M., & Vigna, S. (2007). "A deeper investigation - of PageRank as a function of the damping factor." In Dagstuhl Seminar - Proceedings. Schloss Dagstuhl-Leibniz-Zentrum für Informatik. - """ - alpha0 = alphas.max() - WT = alpha0 * W.T - diff = 1 - it = 1 - - # initialise PageRank vectors - pPageRank = np.zeros(n) - - pPageRank_all = np.zeros((n, len(alphas))) - pPageRank[i] = 1 - - pPageRank_all[i, :] = 1 - - pPageRank_old = pPageRank.copy() - pTotalRank = pPageRank.copy() - - oneminusalpha0 = 1 - alpha0 - - while diff > 1e-9: - # calculate personalised PageRank via power iteration - pPageRank = WT @ pPageRank - pPageRank[i] += oneminusalpha0 - # calculate difference in pPageRank from previous iteration - delta_pPageRank = pPageRank - pPageRank_old - # Eq. [S23] Ref. [1] - pTotalRank += (delta_pPageRank) / ((it + 1) * (alpha0**it)) - # only calculate personalised pageranks if more than one alpha - if len(alphas) > 1: - pPageRank_all += np.outer((delta_pPageRank), (alphas / alpha0) ** it) - - # calculate convergence criteria - diff = np.sum((delta_pPageRank) ** 2) / n - it += 1 - - if it > maxIter: - print(i, "max iterations exceeded") - diff = 0 - pPageRank_old = pPageRank.copy() - - return pPageRank_all, pTotalRank, it -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/basic/predecessor_path_based.html b/docs/_modules/easygraph/functions/basic/predecessor_path_based.html deleted file mode 100644 index d38a0028..00000000 --- a/docs/_modules/easygraph/functions/basic/predecessor_path_based.html +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - easygraph.functions.basic.predecessor_path_based — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.basic.predecessor_path_based

-import easygraph as eg
-
-
-__all__ = [
-    "predecessor",
-]
-
-
-
[docs]def predecessor(G, source, target=None, cutoff=None, return_seen=None): - """Returns dict of predecessors for the path from source to all nodes in G. - - Parameters - ---------- - G : EasyGraph graph - - source : node label - Starting node for path - - target : node label, optional - Ending node for path. If provided only predecessors between - source and target are returned - - cutoff : integer, optional - Depth to stop the search. Only paths of length <= cutoff are returned. - - return_seen : bool, optional (default=None) - Whether to return a dictionary, keyed by node, of the level (number of - hops) to reach the node (as seen during breadth-first-search). - - Returns - ------- - pred : dictionary - Dictionary, keyed by node, of predecessors in the shortest path. - - - (pred, seen): tuple of dictionaries - If `return_seen` argument is set to `True`, then a tuple of dictionaries - is returned. The first element is the dictionary, keyed by node, of - predecessors in the shortest path. The second element is the dictionary, - keyed by node, of the level (number of hops) to reach the node (as seen - during breadth-first-search). - - Examples - -------- - >>> G = eg.path_graph(4) - >>> list(G) - [0, 1, 2, 3] - >>> eg.predecessor(G, 0) - {0: [], 1: [0], 2: [1], 3: [2]} - >>> eg.predecessor(G, 0, return_seen=True) - ({0: [], 1: [0], 2: [1], 3: [2]}, {0: 0, 1: 1, 2: 2, 3: 3}) - - - """ - - if source not in G: - raise eg.NodeNotFound(f"Source {source} not in G") - level = 0 # the current level - nextlevel = [source] # list of nodes to check at next level - seen = {source: level} # level (number of hops) when seen in BFS - pred = {source: []} # predecessor dictionary - while nextlevel: - level = level + 1 - thislevel = nextlevel - nextlevel = [] - for v in thislevel: - for w in list(G.neighbors(v)): - if w not in seen: - pred[w] = [v] - seen[w] = level - nextlevel.append(w) - elif seen[w] == level: # add v to predecessor list if it - pred[w].append(v) # is at the correct level - if cutoff and cutoff <= level: - break - - if target is not None: - if return_seen: - if target not in pred: - return ([], -1) # No predecessor - return (pred[target], seen[target]) - else: - if target not in pred: - return [] # No predecessor - return pred[target] - else: - if return_seen: - return (pred, seen) - else: - return pred
- - -# def main(): -# G = eg.path_graph(4) -# print(G.edges) - -# print(predecessor(G, 0)) - - -# if __name__ == "__main__": -# main() -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/centrality/betweenness.html b/docs/_modules/easygraph/functions/centrality/betweenness.html deleted file mode 100644 index 9c9c9129..00000000 --- a/docs/_modules/easygraph/functions/centrality/betweenness.html +++ /dev/null @@ -1,358 +0,0 @@ - - - - - - easygraph.functions.centrality.betweenness — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.centrality.betweenness

-from easygraph.utils import *
-from easygraph.utils.decorators import *
-
-
-__all__ = [
-    "betweenness_centrality",
-]
-
-
-def betweenness_centrality_parallel(nodes, G, path_length, accumulate):
-    betweenness = {node: 0.0 for node in G}
-    for node in nodes:
-        S, P, sigma = path_length(G, source=node)
-        betweenness = accumulate(betweenness, S, P, sigma, node)
-    return betweenness
-
-
-
[docs]@not_implemented_for("multigraph") -@hybrid("cpp_betweenness_centrality") -def betweenness_centrality( - G, weight=None, sources=None, normalized=True, endpoints=False, n_workers=None -): - r"""Compute the shortest-basic betweenness centrality for nodes. - - .. math:: - - c_B(v) = \sum_{s,t \in V} \frac{\sigma(s, t|v)}{\sigma(s, t)} - - where V is the set of nodes, - - .. math:: - \sigma(s, t) - - is the number of shortest (s, t)-paths, and - - .. math:: - - \sigma(s, t|v) - - is the number of those paths passing through some node v other than s, t. - - .. math:: - - If\ s\ =\ t,\ \sigma(s, t) = 1, and\ if\ v \in {s, t}, \sigma(s, t|v) = 0 [2]_. - - Parameters - ---------- - G : graph - A easygraph graph. - - weight : None or string, optional (default=None) - If None, all edge weights are considered equal. - Otherwise holds the name of the edge attribute used as weight. - - sources : None or nodes list, optional (default=None) - If None, all nodes are considered. - Otherwise,the set of source vertices to consider when calculating shortest paths. - - normalized : bool, optional - If True the betweenness values are normalized by `2/((n-1)(n-2))` - for graphs, and `1/((n-1)(n-2))` for directed graphs where `n` - is the number of nodes in G. - - endpoints : bool, optional - If True include the endpoints in the shortest basic counts. - - Returns - ------- - - nodes : dictionary - Dictionary of nodes with betweenness centrality as the value. - - >>> betweenness_centrality(G,weight="weight") - """ - - import functools - - if weight is not None: - path_length = functools.partial(_single_source_dijkstra_path, weight=weight) - else: - path_length = functools.partial(_single_source_bfs_path) - - if endpoints: - accumulate = functools.partial(_accumulate_endpoints) - else: - accumulate = functools.partial(_accumulate_basic) - - if sources is not None: - nodes = sources - else: - nodes = G.nodes - betweenness = dict.fromkeys(G, 0.0) - - if n_workers is not None: - # use the parallel version for large graph - import random - - from functools import partial - from multiprocessing import Pool - - nodes = list(nodes) - random.shuffle(nodes) - - if len(nodes) > n_workers * 30000: - nodes = split_len(nodes, step=30000) - else: - nodes = split(nodes, n_workers) - local_function = partial( - betweenness_centrality_parallel, - G=G, - path_length=path_length, - accumulate=accumulate, - ) - with Pool(n_workers) as p: - ret = p.imap(local_function, nodes) - for res in ret: - for key in res: - betweenness[key] += res[key] - else: - # use np-parallel version for small graph - for node in nodes: - S, P, sigma = path_length(G, source=node) - betweenness = accumulate(betweenness, S, P, sigma, node) - - betweenness = _rescale( - betweenness, - len(G), - normalized=normalized, - directed=G.is_directed(), - endpoints=endpoints, - ) - ret = [0.0 for i in range(len(G))] - for i in range(len(ret)): - ret[i] = betweenness[G.index2node[i]] - return ret
- - -def _rescale(betweenness, n, normalized, directed=False, endpoints=False): - if normalized: - if endpoints: - if n < 2: - scale = None # no normalization - else: - # Scale factor should include endpoint nodes - scale = 1 / (n * (n - 1)) - elif n <= 2: - scale = None # no normalization b=0 for all nodes - else: - scale = 1 / ((n - 1) * (n - 2)) - else: # rescale by 2 for undirected graphs - if not directed: - scale = 0.5 - else: - scale = None - if scale is not None: - for v in betweenness: - betweenness[v] *= scale - return betweenness - - -def _single_source_bfs_path(G, source): - S = [] - P = {v: [] for v in G} - sigma = dict.fromkeys(G, 0.0) - D = {} - sigma[source] = 1.0 - D[source] = 0 - Q = [source] - adj = G.adj - while Q: - v = Q.pop(0) - S.append(v) - Dv = D[v] - sigmav = sigma[v] - for w in adj[v]: - if w not in D: - Q.append(w) - D[w] = Dv + 1 - if D[w] == Dv + 1: - sigma[w] += sigmav - P[w].append(v) - return S, P, sigma - - -def _single_source_dijkstra_path(G, source, weight="weight"): - from heapq import heappop - from heapq import heappush - - push = heappush - pop = heappop - S = [] - P = {v: [] for v in G} - sigma = dict.fromkeys(G, 0.0) - D = {} - sigma[source] = 1.0 - seen = {source: 0} - Q = [] - from itertools import count - - c = count() - adj = G.adj - push(Q, (0, next(c), source, source)) - while Q: - (dist, _, pred, v) = pop(Q) - if v in D: - continue - sigma[v] += sigma[pred] - S.append(v) - D[v] = dist - for w in adj[v]: - vw_dist = dist + adj[v][w].get(weight, 1) - if w not in D and (w not in seen or vw_dist < seen[w]): - seen[w] = vw_dist - push(Q, (vw_dist, next(c), v, w)) - sigma[w] = 0.0 - P[w] = [v] - elif vw_dist == seen[w]: # handle equal paths - sigma[w] += sigma[v] - P[w].append(v) - return S, P, sigma - - -def _accumulate_endpoints(betweenness, S, P, sigma, s): - betweenness[s] += len(S) - 1 - delta = dict.fromkeys(S, 0) - while S: - w = S.pop() - coeff = (1 + delta[w]) / sigma[w] - for v in P[w]: - delta[v] += sigma[v] * coeff - if w != s: - betweenness[w] += delta[w] + 1 - return betweenness - - -def _accumulate_basic(betweenness, S, P, sigma, s): - delta = dict.fromkeys(S, 0) - while S: - w = S.pop() - coeff = (1 + delta[w]) / sigma[w] - for v in P[w]: - delta[v] += sigma[v] * coeff - if w != s: - betweenness[w] += delta[w] - return betweenness -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/centrality/closeness.html b/docs/_modules/easygraph/functions/centrality/closeness.html deleted file mode 100644 index 080e8f01..00000000 --- a/docs/_modules/easygraph/functions/centrality/closeness.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - easygraph.functions.centrality.closeness — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.centrality.closeness

-from easygraph.functions.basic import *
-from easygraph.functions.path import single_source_bfs
-from easygraph.functions.path import single_source_dijkstra
-from easygraph.utils import *
-
-
-__all__ = [
-    "closeness_centrality",
-]
-
-
-def closeness_centrality_parallel(nodes, G, path_length):
-    ret = []
-    length = len(G)
-    for node in nodes:
-        x = path_length(G, node)
-        dist = sum(x.values())
-        cnt = len(x)
-        if dist == 0:
-            ret.append([node, 0])
-        else:
-            ret.append([node, (cnt - 1) * (cnt - 1) / (dist * (length - 1))])
-    return ret
-
-
-
[docs]@not_implemented_for("multigraph") -@hybrid("cpp_closeness_centrality") -def closeness_centrality(G, weight=None, sources=None, n_workers=None): - r""" - Compute closeness centrality for nodes. - - .. math:: - - C_{WF}(u) = \frac{n-1}{N-1} \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)}, - - Notice that the closeness distance function computes the - outcoming distance to `u` for directed graphs. To use - incoming distance, act on `G.reverse()`. - - Parameters - ---------- - G : graph - A easygraph graph - - weight : None or string, optional (default=None) - If None, all edge weights are considered equal. - Otherwise holds the name of the edge attribute used as weight. - - sources : None or nodes list, optional (default=None) - If None, all nodes are returned - Otherwise,the set of source vertices to creturn. - - Returns - ------- - nodes : dictionary - Dictionary of nodes with closeness centrality as the value. - """ - closeness = dict() - if sources is not None: - nodes = sources - else: - nodes = G.nodes - length = len(G) - import functools - - if weight is not None: - path_length = functools.partial(single_source_dijkstra, weight=weight) - else: - path_length = functools.partial(single_source_bfs) - - if n_workers is not None: - # use parallel version for large graph - import random - - from functools import partial - from multiprocessing import Pool - - nodes = list(nodes) - random.shuffle(nodes) - - if len(nodes) > n_workers * 30000: - nodes = split_len(nodes, step=30000) - else: - nodes = split(nodes, n_workers) - local_function = partial( - closeness_centrality_parallel, G=G, path_length=path_length - ) - with Pool(n_workers) as p: - ret = p.imap(local_function, nodes) - res = [x for i in ret for x in i] - closeness = dict(res) - else: - # use np-parallel version for small graph - for node in nodes: - x = path_length(G, node) - dist = sum(x.values()) - cnt = len(x) - if dist == 0: - closeness[node] = 0 - else: - closeness[node] = (cnt - 1) * (cnt - 1) / (dist * (length - 1)) - ret = [0.0 for i in range(len(G))] - for i in range(len(ret)): - ret[i] = closeness[G.index2node[i]] - return ret
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/centrality/degree.html b/docs/_modules/easygraph/functions/centrality/degree.html deleted file mode 100644 index dd8ed29c..00000000 --- a/docs/_modules/easygraph/functions/centrality/degree.html +++ /dev/null @@ -1,235 +0,0 @@ - - - - - - easygraph.functions.centrality.degree — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.centrality.degree

-from easygraph.utils.decorators import *
-
-
-__all__ = ["degree_centrality", "in_degree_centrality", "out_degree_centrality"]
-
-
-
[docs]@not_implemented_for("multigraph") -def degree_centrality(G): - """Compute the degree centrality for nodes in a bipartite network. - - The degree centrality for a node v is the fraction of nodes it - is connected to. - - parameters - ---------- - G : graph - A easygraph graph - - Returns - ------- - nodes : dictionary - Dictionary of nodes with degree centrality as the value. - - Notes - ----- - The degree centrality are normalized by dividing by n-1 where - n is number of nodes in G. - """ - if len(G) <= 1: - return {n: 1 for n in G} - - s = 1.0 / (len(G) - 1.0) - centrality = {n: d * s for n, d in (G.degree()).items()} - return centrality
- - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_Directed_graph -def in_degree_centrality(G): - """Compute the in-degree centrality for nodes. - - The in-degree centrality for a node v is the fraction of nodes its - incoming edges are connected to. - - Parameters - ---------- - G : graph - A EasyGraph graph - - Returns - ------- - nodes : dictionary - Dictionary of nodes with in-degree centrality as values. - - Raises - ------ - EasyGraphNotImplemented: - If G is undirected. - - See Also - -------- - degree_centrality, out_degree_centrality - - Notes - ----- - The degree centrality values are normalized by dividing by the maximum - possible degree in a simple graph n-1 where n is the number of nodes in G. - - For multigraphs or graphs with self loops the maximum degree might - be higher than n-1 and values of degree centrality greater than 1 - are possible. - """ - if len(G) <= 1: - return {n: 1 for n in G} - - s = 1.0 / (len(G) - 1.0) - centrality = {n: d * s for n, d in G.in_degree()} - return centrality
- - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_Directed_graph -def out_degree_centrality(G): - """Compute the out-degree centrality for nodes. - - The out-degree centrality for a node v is the fraction of nodes its - outgoing edges are connected to. - - Parameters - ---------- - G : graph - A EasyGraph graph - - Returns - ------- - nodes : dictionary - Dictionary of nodes with out-degree centrality as values. - - Raises - ------ - EasyGraphNotImplemented: - If G is undirected. - - See Also - -------- - degree_centrality, in_degree_centrality - - Notes - ----- - The degree centrality values are normalized by dividing by the maximum - possible degree in a simple graph n-1 where n is the number of nodes in G. - - For multigraphs or graphs with self loops the maximum degree might - be higher than n-1 and values of degree centrality greater than 1 - are possible. - """ - if len(G) <= 1: - return {n: 1 for n in G} - - s = 1.0 / (len(G) - 1.0) - centrality = {n: d * s for n, d in G.out_degree()} - return centrality
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/centrality/ego_betweenness.html b/docs/_modules/easygraph/functions/centrality/ego_betweenness.html deleted file mode 100644 index 4699ffe2..00000000 --- a/docs/_modules/easygraph/functions/centrality/ego_betweenness.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - easygraph.functions.centrality.ego_betweenness — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • »
  • -
  • Module code »
  • -
  • easygraph.functions.centrality.ego_betweenness
  • -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.centrality.ego_betweenness

-__all__ = ["ego_betweenness"]
-import numpy as np
-
-from easygraph.utils import *
-
-
-
[docs]@not_implemented_for("multigraph") -def ego_betweenness(G, node): - """ - ego networks are networks consisting of a single actor (ego) together with the actors they are connected to (alters) and all the links among those alters.[1] - Burt (1992), in his book Structural Holes, provides ample evidence that having high betweenness centrality, which is highly correlated with having many structural holes, can bring benefits to ego.[1] - Returns the betweenness centrality of a ego network whose ego is set - - Parameters - ---------- - G : graph - node : int - - Returns - ------- - sum : float - the betweenness centrality of a ego network whose ego is set - - Examples - -------- - Returns the betwenness centrality of node 1. - - >>> ego_betweenness(G,node=1) - - Reference - --------- - .. [1] Martin Everett, Stephen P. Borgatti. "Ego network betweenness." Social Networks, Volume 27, Issue 1, Pages 31-38, 2005. - - """ - g = G.ego_subgraph(node) - print(g.edges) - print(g.nodes) - n = len(g) - - A = np.zeros((n, n)) - - for i in range(n): - for j in range(n): - if g.has_edge(g.index2node[i], g.index2node[j]): - A[i, j] = 1 - - B = A * A - C = np.identity(n) - A - sum = 0 - flag = G.is_directed() - for i in range(n): - for j in range(n): - if i != j and C[i, j] == 1 and B[i, j] != 0: - sum += 1.0 / B[i, j] - if flag == False: - sum /= 2 - return sum
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/centrality/flowbetweenness.html b/docs/_modules/easygraph/functions/centrality/flowbetweenness.html deleted file mode 100644 index f4b72f7a..00000000 --- a/docs/_modules/easygraph/functions/centrality/flowbetweenness.html +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - easygraph.functions.centrality.flowbetweenness — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.centrality.flowbetweenness

-import collections
-import copy
-
-from easygraph.utils.decorators import *
-
-
-__all__ = [
-    "flowbetweenness_centrality",
-]
-
-
-
[docs]@not_implemented_for("multigraph") -def flowbetweenness_centrality(G): - """Compute the independent-basic betweenness centrality for nodes in a flow network. - - .. math:: - - c_B(v) =\\sum_{s,t \\in V} \frac{\\sigma(s, t|v)}{\\sigma(s, t)} - - where V is the set of nodes, - - .. math:: - - \\sigma(s, t)\\ is\\ the\\ number\\ of\\ independent\\ (s, t)-paths, - - .. math:: - - \\sigma(s, t|v)\\ is\\ the\\ maximum\\ number\\ possible\\ of\\ those\\ paths\\ passing\\ through\\ some\\ node\\ v\\ other\\ than\\ s, t.\ - - .. math:: - - If\\ s\\ =\\ t,\\ \\sigma(s, t)\\ =\\ 1,\\ and\\ if\\ v \\in \\{s, t\\},\\ \\sigma(s, t|v)\\ =\\ 0\\ [2]_. - - Parameters - ---------- - G : graph - A easygraph directed graph. - - Returns - ------- - nodes : dictionary - Dictionary of nodes with independent-basic betweenness centrality as the value. - - Notes - ----- - A flow network is a directed graph where each edge has a capacity and each edge receives a flow. - """ - if G.is_directed() == False: - print("Please input a directed graph") - return - flow_dict = NumberOfFlow(G) - nodes = G.nodes - result_dict = dict() - for node, _ in nodes.items(): - result_dict[node] = 0 - for node_v, _ in nodes.items(): - for node_s, _ in nodes.items(): - for node_t, _ in nodes.items(): - num = 1 - num_v = 0 - if node_s == node_t: - num_v = 0 - num = 1 - if node_v in [node_s, node_t]: - num_v = 0 - num = 1 - if node_v != node_s and node_v != node_t and node_s != node_t: - num = flow_dict[node_s][node_t] - num_v = min(flow_dict[node_s][node_v], flow_dict[node_v][node_t]) - if num == 0: - pass - else: - result_dict[node_v] = result_dict[node_v] + num_v / num - return result_dict
- - -# flow betweenness -def NumberOfFlow(G): - nodes = G.nodes - result_dict = dict() - for node1, _ in nodes.items(): - result_dict[node1] = dict() - for node2, _ in nodes.items(): - if node1 == node2: - pass - else: - result_dict[node1][node2] = edmonds_karp(G, node1, node2) - return result_dict - - -def edmonds_karp(G, source, sink): - nodes = G.nodes - parent = dict() - for node, _ in nodes.items(): - parent[node] = -1 - - adj = copy.deepcopy(G.adj) - max_flow = 0 - while bfs(G, source, sink, parent, adj): - path_flow = float("inf") - s = sink - while s != source: - path_flow = min(path_flow, adj[parent[s]][s].get("weight", 1)) - s = parent[s] - max_flow += path_flow - v = sink - while v != source: - u = parent[v] - x = adj[u][v].get("weight", 1) - adj[u][v].update({"weight": x}) - adj[u][v]["weight"] -= path_flow - - flag = 0 - if v not in adj: - adj[v] = dict() - if u not in adj[v]: - adj[v][u] = dict() - flag = 1 - if flag == 1: - x = 0 - else: - x = adj[v][u].get("weight", 1) - adj[v][u].update({"weight": x}) - adj[v][u]["weight"] += path_flow - v = parent[v] - return max_flow - - -def bfs(G, source, sink, parent, adj): - nodes = G.nodes - visited = dict() - for node, _ in nodes.items(): - visited[node] = 0 - queue = collections.deque() - queue.append(source) - visited[source] = True - while queue: - u = queue.popleft() - if u not in adj: - continue - for v, attr in adj[u].items(): - if (visited[v] == False) and (attr.get("weight", 1) > 0): - queue.append(v) - visited[v] = True - parent[v] = u - return visited[sink] -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/centrality/laplacian.html b/docs/_modules/easygraph/functions/centrality/laplacian.html deleted file mode 100644 index f20e2f77..00000000 --- a/docs/_modules/easygraph/functions/centrality/laplacian.html +++ /dev/null @@ -1,247 +0,0 @@ - - - - - - easygraph.functions.centrality.laplacian — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.centrality.laplacian

-from easygraph.utils import *
-
-
-__all__ = ["laplacian"]
-
-
-
[docs]@not_implemented_for("multigraph") -def laplacian(G, n_workers=None): - """Returns the laplacian centrality of each node in the weighted graph - - Parameters - ---------- - G : graph - weighted graph - - Returns - ------- - CL : dict - the laplacian centrality of each node in the weighted graph - - Examples - -------- - Returns the laplacian centrality of each node in the weighted graph G - - >>> laplacian(G) - - Reference - --------- - .. [1] Xingqin Qi, Eddie Fuller, Qin Wu, Yezhou Wu, Cun-Quan Zhang. - "Laplacian centrality: A new centrality measure for weighted networks." - Information Sciences, Volume 194, Pages 240-253, 2012. - - """ - adj = G.adj - from collections import defaultdict - - X = defaultdict(int) - W = defaultdict(int) - CL = {} - - if n_workers is not None: - # use the parallel version for large graph - import random - - from functools import partial - from multiprocessing import Pool - - nodes = list(G.nodes) - random.shuffle(nodes) - - if len(nodes) > n_workers * 30000: - nodes = split_len(nodes, step=30000) - else: - nodes = split(nodes, n_workers) - - local_function = partial(initialize_parallel, G=G, adj=adj) - with Pool(n_workers) as p: - ret = p.imap(local_function, nodes) - resX, resW = [], [] - for i in ret: - for x in i: - resX.append(x[0]) - resW.append(x[1]) - X = dict(resX) - W = dict(resW) - ELG = sum(X[i] * X[i] for i in G) + sum(W[i] for i in G) - local_function = partial(laplacian_parallel, G=G, X=X, W=W, adj=adj, ELG=ELG) - with Pool(n_workers) as p: - ret = p.imap(local_function, nodes) - res = [x for i in ret for x in i] - CL = dict(res) - - else: - # use np-parallel version for small graph - for i in G: - for j in G: - if i in G and j in G[i]: - X[i] += adj[i][j].get("weight", 1) - W[i] += adj[i][j].get("weight", 1) * adj[i][j].get("weight", 1) - ELG = sum(X[i] * X[i] for i in G) + sum(W[i] for i in G) - for i in G: - import copy - - Xi = copy.deepcopy(X) - for j in G: - if j in adj.keys() and i in adj[j].keys(): - Xi[j] -= adj[j][i].get("weight", 1) - Xi[i] = 0 - ELGi = sum(Xi[i] * Xi[i] for i in G) + sum(W[i] for i in G) - 2 * W[i] - if ELG: - CL[i] = (float)(ELG - ELGi) / ELG - return CL
- - -def initialize_parallel(nodes, G, adj): - ret = [] - for i in nodes: - X = 0 - W = 0 - for j in G: - if j in G[i]: - X += adj[i][j].get("weight", 1) - W += adj[i][j].get("weight", 1) * adj[i][j].get("weight", 1) - ret.append([[i, X], [i, W]]) - return ret - - -def laplacian_parallel(nodes, G, X, W, adj, ELG): - ret = [] - for i in nodes: - import copy - - Xi = copy.deepcopy(X) - for j in G: - if j in adj.keys() and i in adj[j].keys(): - Xi[j] -= adj[j][i].get("weight", 1) - Xi[i] = 0 - ELGi = sum(Xi[i] * Xi[i] for i in G) + sum(W[i] for i in G) - 2 * W[i] - if ELG: - ret.append([i, (float)(ELG - ELGi) / ELG]) - return ret - - -def sort(data): - return dict(sorted(data.items(), key=lambda x: x[0], reverse=True)) - - -def output(data, path): - import json - - data = sort(data) - json_str = json.dumps(data, ensure_ascii=False, indent=4) - with open(path, "w", encoding="utf-8") as json_file: - json_file.write(json_str) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/centrality/pagerank.html b/docs/_modules/easygraph/functions/centrality/pagerank.html deleted file mode 100644 index ee96a86f..00000000 --- a/docs/_modules/easygraph/functions/centrality/pagerank.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - easygraph.functions.centrality.pagerank — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.centrality.pagerank

-import easygraph as eg
-
-from easygraph.utils import *
-
-
-__all__ = ["pagerank"]
-
-
-
[docs]@not_implemented_for("multigraph") -@hybrid("cpp_pagerank") -def pagerank(G, alpha=0.85): - """ - Returns the PageRank value of each node in G. - - Parameters - ---------- - G : graph - Undirected graph will be considered as directed graph with two directed edges for each undirected edge. - - alpha : float - The damping factor. Default is 0.85 - - """ - import numpy as np - - if len(G) == 0: - return {} - M = google_matrix(G, alpha=alpha) - - # use numpy LAPACK solver - eigenvalues, eigenvectors = np.linalg.eig(M.T) - ind = np.argmax(eigenvalues) - # eigenvector of largest eigenvalue is at ind, normalized - largest = np.array(eigenvectors[:, ind]).flatten().real - norm = float(largest.sum()) - return dict(zip(G, map(float, largest / norm)))
- - -def google_matrix(G, alpha): - import numpy as np - - M = eg.to_numpy_array(G) - N = len(G) - if N == 0: - return M - - # Get dangling nodes(nodes with no out link) - dangling_nodes = np.where(M.sum(axis=1) == 0)[0] - dangling_weights = np.repeat(1.0 / N, N) - for node in dangling_nodes: - M[node] = dangling_weights - - M /= M.sum(axis=1)[:, np.newaxis] - - return alpha * M + (1 - alpha) * np.repeat(1.0 / N, N) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/community/LPA.html b/docs/_modules/easygraph/functions/community/LPA.html deleted file mode 100644 index 829302b1..00000000 --- a/docs/_modules/easygraph/functions/community/LPA.html +++ /dev/null @@ -1,874 +0,0 @@ - - - - - - easygraph.functions.community.LPA — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.community.LPA

-import copy
-import random
-
-from collections import defaultdict
-from queue import Queue
-
-import easygraph as eg
-import numpy as np
-
-from easygraph.utils import *
-
-
-__all__ = [
-    "LPA",
-    "SLPA",
-    "HANP",
-    "BMLPA",
-]
-
-
-
[docs]@not_implemented_for("multigraph") -def LPA(G): - """Detect community by label propagation algorithm - Return the detected communities. But the result is random. - Each node in the network is initially assigned to its own community. At every iteration,nodes have - a label that the maximum number of their neighbors have. If there are more than one nodes fit and - available, choose a label randomly. Finally, nodes having the same labels are grouped together as - communities. In case two or more disconnected groups of nodes have the same label, we run a simple - breadth-first search to separate the disconnected communities - - Parameters - ---------- - G : graph - A easygraph graph - - Returns - ---------- - communities : dictionary - key: serial number of community , value: nodes in the community. - - Examples - ---------- - >>> LPA(G) - - References - ---------- - .. [1] Usha Nandini Raghavan, Réka Albert, and Soundar Kumara: - Near linear time algorithm to detect community structures in large-scale networks - """ - i = 0 - label_dict = dict() - cluster_community = dict() - Next_label_dict = dict() - nodes = list(G.nodes.keys()) - if len(nodes) == 1: - return {1: [nodes[0]]} - for node in nodes: - label_dict[node] = i - i = i + 1 - loop_count = 0 - while True: - loop_count += 1 - random.shuffle(nodes) - for node in nodes: - labels = SelectLabels(G, node, label_dict) - if labels == []: - Next_label_dict[node] = label_dict[node] - continue - Next_label_dict[node] = random.choice(labels) - # Asynchronous updates. If you want to use synchronous updates, comment the line below - label_dict[node] = Next_label_dict[node] - label_dict = Next_label_dict - if estimate_stop_cond(G, label_dict) is True: - break - for node in label_dict.keys(): - label = label_dict[node] - if label not in cluster_community.keys(): - cluster_community[label] = [node] - else: - cluster_community[label].append(node) - - result_community = CheckConnectivity(G, cluster_community) - return result_community
- - -
[docs]@not_implemented_for("multigraph") -def SLPA(G, T, r): - """Detect Overlapping Communities by Speaker-listener Label Propagation Algorithm - Return the detected Overlapping communities. But the result is random. - - Parameters - ---------- - G : graph - A easygraph graph. - T : int - The number of iterations, In general, T is set greater than 20, which produces relatively stable outputs. - r : int - a threshold between 0 and 1. - - Returns - ------- - communities : dictionary - key: serial number of community , value: nodes in the community. - - Examples - ---------- - >>> SLPA(G, - ... T = 20, - ... r = 0.05 - ... ) - - References - ---------- - .. [1] Jierui Xie, Boleslaw K. Szymanski, Xiaoming Liu: - SLPA: Uncovering Overlapping Communities in Social Networks via A Speaker-listener Interaction Dynamic Process - """ - nodes = list(G.nodes.keys()) - if len(nodes) == 1: - return {1: [nodes[0]]} - nodes = G.nodes - adj = G.adj - memory = {i: {i: 1} for i in nodes} - for i in range(0, T): - listenerslist = list(G.nodes) - random.shuffle(listenerslist) - for listener in listenerslist: - speakerlist = adj[listener] - if len(speakerlist) == 0: - continue - labels = defaultdict(int) - for speaker in speakerlist: - # Speaker Rule - total = float(sum(memory[speaker].values())) - keys = list(memory[speaker].keys()) - index = np.random.multinomial( - 1, [round(freq / total, 2) for freq in memory[speaker].values()] - ).argmax() - chosen_label = keys[index] - labels[chosen_label] += 1 - # Listener Rule - maxlabel = max(labels.items(), key=lambda x: x[1])[0] - if maxlabel in memory[listener]: - memory[listener][maxlabel] += 1 - else: - memory[listener][maxlabel] = 1 - - for node, labels in memory.items(): - name_list = [] - for label_name, label_number in labels.items(): - if round(label_number / float(T + 1), 2) < r: - name_list.append(label_name) - for name in name_list: - del labels[name] - - # Find nodes membership - communities = {} - for node, labels in memory.items(): - for label in labels: - if label in communities: - communities[label].add(node) - else: - communities[label] = {node} - - # Remove nested communities - RemoveNested(communities) - - # Check Connectivity - result_community = CheckConnectivity(G, communities) - return result_community
- - -
[docs]@not_implemented_for("multigraph") -def HANP(G, m, delta, threshod=1, hier_open=0, combine_open=0): - """Detect community by Hop attenuation & node preference algorithm - - Return the detected communities. But the result is random. - - Implement the basic HANP algorithm and give more freedom through the parameters, e.g., you can use threshod - to set the condition for node updating. If network are known to be Hierarchical and overlapping communities, - it's recommended to choose geodesic distance as the measure(instead of receiving the current hop scores - from the neighborhood and carry out a subtraction) and When an equilibrium is reached, treat newly combined - communities as a single node. - - For using Floyd to get the shortest distance, the time complexity is a little high. - - Parameters - ---------- - G : graph - A easygraph graph - m : float - Used to calculate score, when m > 0, more preference is given to node with more neighbors; m < 0, less - delta : float - Hop attenuation - threshod : float - Between 0 and 1, only update node whose number of neighbors sharing the maximal label is less than the threshod. - e.g., threshod == 1 means updating all nodes. - hier_open : - 1 means using geodesic distance as the score measure. - 0 means not. - combine_open : - this option is valid only when hier_open = 1 - 1 means When an equilibrium is reached, treat newly combined communities as a single node. - 0 means not. - - Returns - ---------- - communities : dictionary - key: serial number of community , value: nodes in the community. - - Examples - ---------- - >>> HANP(G, - ... m = 0.1, - ... delta = 0.05, - ... threshod = 1, - ... hier_open = 0, - ... combine_open = 0 - ... ) - - References - ---------- - .. [1] Ian X. Y. Leung, Pan Hui, Pietro Liò, and Jon Crowcrof: - Towards real-time community detection in large networks - - """ - nodes = list(G.nodes.keys()) - if len(nodes) == 1: - return {1: [nodes[0]]} - label_dict = dict() - score_dict = dict() - node_dict = dict() - Next_label_dict = dict() - cluster_community = dict() - nodes = list(G.nodes.keys()) - degrees = G.degree() - records = [] - loop_count = 0 - i = 0 - old_score = 1 - ori_G = G - if hier_open == 1: - distance_dict = eg.Floyd(G) - for node in nodes: - label_dict[node] = i - score_dict[i] = 1 - node_dict[i] = node - i = i + 1 - while True: - loop_count += 1 - random.shuffle(nodes) - score = 1 - for node in nodes: - labels = SelectLabels_HANP( - G, node, label_dict, score_dict, degrees, m, threshod - ) - if labels == []: - Next_label_dict[node] = label_dict[node] - continue - old_label = label_dict[node] - Next_label_dict[node] = random.choice(labels) - # Asynchronous updates. If you want to use synchronous updates, comment the line below - label_dict[node] = Next_label_dict[node] - if hier_open == 1: - score_dict[Next_label_dict[node]] = UpdateScore_Hier( - G, node, label_dict, node_dict, distance_dict - ) - score = min(score, score_dict[Next_label_dict[node]]) - else: - if old_label == Next_label_dict[node]: - cdelta = 0 - else: - cdelta = delta - score_dict[Next_label_dict[node]] = UpdateScore( - G, node, label_dict, score_dict, cdelta - ) - if hier_open == 1 and combine_open == 1: - if old_score - score > 1 / 3: - old_score = score - ( - records, - G, - label_dict, - score_dict, - node_dict, - Next_label_dict, - nodes, - degrees, - distance_dict, - ) = CombineNodes( - records, - G, - label_dict, - score_dict, - node_dict, - Next_label_dict, - nodes, - degrees, - distance_dict, - ) - label_dict = Next_label_dict - if ( - estimate_stop_cond_HANP(G, label_dict, score_dict, degrees, m, threshod) - is True - ): - break - """As mentioned in the paper, it's suggested that the number of iterations - required is independent to the number of nodes and that after - five iterations, 95% of their nodes are already accurately clustered - """ - if loop_count > 20: - break - print("After %d iterations, HANP complete." % loop_count) - for node in label_dict.keys(): - label = label_dict[node] - if label not in cluster_community.keys(): - cluster_community[label] = [node] - else: - cluster_community[label].append(node) - if hier_open == 1 and combine_open == 1: - records.append(cluster_community) - cluster_community = ShowRecord(records) - result_community = CheckConnectivity(ori_G, cluster_community) - return result_community
- - -
[docs]@not_implemented_for("multigraph") -def BMLPA(G, p): - """Detect community by Balanced Multi-Label Propagation algorithm - - Return the detected communities. - - Firstly, initialize 'old' using cores generated by RC function, the propagate label till the number and size - of communities stay no change, check if there are subcommunity and delete it. Finally, split discontinuous - communities. - - For some directed graphs lead to oscillations of labels, modify the stop condition. - - Parameters - ---------- - G : graph - A easygraph graph - p : float - Between 0 and 1, judge Whether a community identifier should be retained - - Returns - ---------- - communities : dictionary - key: serial number of community , value: nodes in the community. - - Examples - ---------- - >>> BMLPA(G, - ... p = 0.1, - ... ) - - References - ---------- - .. [1] Wu Zhihao, Lin You-Fang, Gregory Steve, Wan Huai-Yu, Tian Sheng-Feng - Balanced Multi-Label Propagation for Overlapping Community Detection in Social Networks - - """ - nodes = list(G.nodes.keys()) - if len(nodes) == 1: - return {1: [nodes[0]]} - cores = Rough_Cores(G) - nodes = G.nodes - i = 0 - old_label_dict = dict() - new_label_dict = dict() - for core in cores: - for node in core: - if node not in old_label_dict: - old_label_dict[node] = {i: 1} - else: - old_label_dict[node][i] = 1 - i += 1 - oldMin = dict() - loop_count = 0 - old_label_dictx = dict() - while True: - loop_count += 1 - old_label_dictx = old_label_dict - for node in nodes: - Propagate_bbc(G, node, old_label_dict, new_label_dict, p) - if loop_count > 50 and old_label_dict == old_label_dictx: - break - Min = dict() - if Id(old_label_dict) == Id(new_label_dict): - Min = mc(count(old_label_dict), count(new_label_dict)) - else: - Min = count(new_label_dict) - if loop_count > 500: - break - if Min != oldMin: - old_label_dict = copy.deepcopy(new_label_dict) - oldMin = copy.deepcopy(Min) - else: - break - print("After %d iterations, BMLPA complete." % loop_count) - communities = dict() - for node in nodes: - for label, _ in old_label_dict[node].items(): - if label in communities: - communities[label].add(node) - else: - communities[label] = {node} - RemoveNested(communities) - result_community = CheckConnectivity(G, communities) - return result_community
- - -def RemoveNested(communities): - nestedCommunities = set() - keys = list(communities.keys()) - for i, label0 in enumerate(keys[:-1]): - comm0 = communities[label0] - for label1 in keys[i + 1 :]: - comm1 = communities[label1] - if comm0.issubset(comm1): - nestedCommunities.add(label0) - elif comm0.issuperset(comm1): - nestedCommunities.add(label1) - for comm in nestedCommunities: - del communities[comm] - - -def SelectLabels(G, node, label_dict): - adj = G.adj - count = {} - count_items = [] - for neighbor in adj[node]: - neighbor_label = label_dict[neighbor] - count[neighbor_label] = count.get(neighbor_label, 0) + 1 - count_items = sorted(count.items(), key=lambda x: x[1], reverse=True) - labels = [k for k, v in count_items if v == count_items[0][1]] - return labels - - -def estimate_stop_cond(G, label_dict): - for node in G.nodes: - if SelectLabels(G, node, label_dict) != [] and ( - label_dict[node] not in SelectLabels(G, node, label_dict) - ): - return False - return True - - -def SelectLabels_HANP(G, node, label_dict, score_dict, degrees, m, threshod): - adj = G.adj - count = defaultdict(float) - cnt = defaultdict(int) - for neighbor in adj[node]: - neighbor_label = label_dict[neighbor] - cnt[neighbor_label] += 1 - count[neighbor_label] += ( - score_dict[neighbor_label] - * (degrees[neighbor] ** m) - * adj[node][neighbor].get("weight", 1) - ) - count_items = sorted(count.items(), key=lambda x: x[1], reverse=True) - labels = [k for k, v in count_items if v == count_items[0][1]] - # only update node whose number of neighbors sharing the maximal label is less than a certain percentage. - if count_items == []: - return [] - if round(cnt[count_items[0][0]] / len(adj[node]), 2) > threshod: - return [label_dict[node]] - return labels - - -def HopAttenuation_Hier(G, node, label_dict, node_dict, distance_dict): - distance = float("inf") - Max_distance = 0 - adj = G.adj - label = label_dict[node] - ori_node = node_dict[label] - for _, distancex in distance_dict[ori_node].items(): - Max_distance = max(Max_distance, distancex) - for neighbor in adj[node]: - if label_dict[neighbor] == label: - distance = min(distance, distance_dict[ori_node][neighbor]) - return round((1 + distance) / Max_distance, 2) - - -def UpdateScore_Hier(G, node, label_dict, node_dict, distance_dict): - return 1 - HopAttenuation_Hier(G, node, label_dict, node_dict, distance_dict) - - -def UpdateScore(G, node, label_dict, score_dict, delta): - adj = G.adj - Max_score = 0 - label = label_dict[node] - for neighbor in adj[node]: - if label_dict[neighbor] == label: - Max_score = max(Max_score, score_dict[label_dict[neighbor]]) - return Max_score - delta - - -def estimate_stop_cond_HANP(G, label_dict, score_dict, degrees, m, threshod): - for node in G.nodes: - if SelectLabels_HANP( - G, node, label_dict, score_dict, degrees, m, threshod - ) != [] and label_dict[node] not in SelectLabels_HANP( - G, node, label_dict, score_dict, degrees, m, threshod - ): - return False - return True - - -def CombineNodes( - records, - G, - label_dict, - score_dict, - node_dict, - Next_label_dict, - nodes, - degrees, - distance_dict, -): - onerecord = dict() - for node, label in label_dict.items(): - if label in onerecord: - onerecord[label].append(node) - else: - onerecord[label] = [node] - records.append(onerecord) - Gx = eg.Graph() - label_dictx = dict() - score_dictx = dict() - node_dictx = dict() - nodesx = [] - cnt = 0 - for record_label in onerecord: - nodesx.append(cnt) - label_dictx[cnt] = record_label - score_dictx[record_label] = score_dict[record_label] - node_dictx[record_label] = cnt - cnt += 1 - record_labels = list(onerecord.keys()) - i = 0 - edge = dict() - adj = G.adj - for i in range(0, len(record_labels)): - edge[i] = dict() - for j in range(0, len(record_labels)): - if i == j: - continue - inodes = onerecord[record_labels[i]] - jnodes = onerecord[record_labels[j]] - for unode in inodes: - for vnode in jnodes: - if unode in adj and vnode in adj[unode]: - if j not in edge[i]: - edge[i][j] = 0 - edge[i][j] += adj[unode][vnode].get("weight", 1) - for unode in edge: - for vnode, w in edge[unode].items(): - if unode < vnode: - Gx.add_edge(unode, vnode, weight=w) - G = Gx - label_dict = label_dictx - score_dict = score_dictx - node_dict = node_dictx - Next_label_dict = label_dictx - nodes = nodesx - degrees = G.degree() - distance_dict = eg.Floyd(G) - return ( - records, - G, - label_dict, - score_dict, - node_dict, - Next_label_dict, - nodes, - degrees, - distance_dict, - ) - - -def ShowRecord(records): - """ - e.g. - records : [ {1:[1,2,3,4],2:[5,6,7,8],3:[9],4:[10],5:[11],6:[12]}, - {2:[0,1,3],3:[2,4,5]}, - {2:[0,1]} ] - - process : {1:[1,2,3,4],2:[5,6,7,8],3:[9],4:[10],5:[11],6:[12]} -> - {2:[ [1,2,3,4] + [5,6,7,8] + [10] ], 3:[ [9] + [11] + [12] ]} -> - {2:[ ([ [1,2,3,4] + [5,6,7,8] + [10] ]) + ([ [9] + [11] + [12] ] ]) } -> - - return : {2:[1,2,3,4,5,6,7,8,10,9,11,12]} - """ - result = dict() - first = records[0] - for i in range(1, len(records)): - keys = list(first.keys()) - onerecord = records[i] - result = {} - for label, nodes in onerecord.items(): - for unode in nodes: - for vnode in first[keys[unode]]: - if label not in result: - result[label] = [] - result[label].append(vnode) - first = result - return first - - -def CheckConnectivity(G, communities): - result_community = dict() - community = [list(community) for label, community in communities.items()] - communityx = [] - for nodes in community: - BFS(G, nodes, communityx) - i = 0 - for com in communityx: - i += 1 - result_community[i] = com - return result_community - - -def BFS(G, nodes, result): - # check the nodes in G are connected or not. if not, desperate the nodes into different connected subgraphs. - if len(nodes) == 0: - return - if len(nodes) == 1: - result.append(nodes) - return - adj = G.adj - queue = Queue() - queue.put(nodes[0]) - seen = set() - seen.add(nodes[0]) - count = 0 - while queue.empty() == 0: - vertex = queue.get() - count += 1 - for w in adj[vertex]: - if w in nodes and w not in seen: - queue.put(w) - seen.add(w) - if count != len(nodes): - result.append([w for w in seen]) - return BFS(G, [w for w in nodes if w not in seen], result) - else: - result.append(nodes) - return - - -def Rough_Cores(G): - nodes = G.nodes - degrees = G.degree() - adj = G.adj - seen_dict = dict() - label_dict = dict() - cores = [] - i = 0 - for node in nodes: - label_dict[node] = i - seen_dict[node] = 1 - i += 1 - degree_list = sorted(degrees.items(), key=lambda x: x[1], reverse=True) - for node, _ in degree_list: - core = [] - if degrees[node] >= 3 and seen_dict[node] == 1: - for neighbor in adj[node]: - max_degree = 0 - j = node - if seen_dict[neighbor] == 1: - if degrees[neighbor] > max_degree: - max_degree = degrees[neighbor] - j = neighbor - elif degrees[neighbor] == max_degree: - pass - if j != []: - core = [node] + [j] - commNeiber = [i for i in adj[node] if i in adj[j]] - commNeiber = [node for node, _ in degree_list if node in commNeiber] - commNeiber = commNeiber[::-1] - while commNeiber != []: - for h in commNeiber: - core.append(h) - for x in commNeiber: - if x not in adj[h]: - commNeiber.remove(x) - if h in commNeiber: - commNeiber.remove(h) - if len(core) >= 3: - for i in core: - seen_dict[i] = 0 - cores.append(core) - core_node = [] - for core in cores: - core_node += core - for node in nodes: - if node not in core_node: - cores.append([node]) - return cores - - -def Normalizer(l): - Sum = 0 - for identifier, coefficient in l.items(): - Sum += coefficient - for identifier, coefficient in l.items(): - l[identifier] = round(coefficient / Sum, 2) - - -def Propagate_bbc(G, x, source, dest, p): - adj = G.adj - dest[x] = dict() - max_b = 0 - for y in adj[x]: - for identifier, coefficient in source[y].items(): - b = coefficient - if identifier in dest[x]: - dest[x][identifier] += b - else: - dest[x][identifier] = b - max_b = max(dest[x][identifier], max_b) - if max_b == 0: - dest[x] = source[x] - return - for identifier in list(dest[x].keys()): - if dest[x][identifier] / max_b < p: - del dest[x][identifier] - Normalizer(dest[x]) - - -def Id(l): - ids = dict() - for x in l: - ids[x] = Id1(l[x]) - return ids - - -def Id1(x): - ids = [] - for identifier, _ in x.items(): - if identifier not in ids: - ids.append(identifier) - return ids - - -def count(l): - counts = dict() - for x in l: - for identifier, _ in l[x].items(): - if identifier in counts: - counts[identifier] += 1 - else: - counts[identifier] = 1 - return counts - - -def mc(cs1, cs2): - cs = dict() - for identifier, _ in cs1.items(): - cs[identifier] = min(cs1[identifier], cs2[identifier]) - return cs -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/community/ego_graph.html b/docs/_modules/easygraph/functions/community/ego_graph.html deleted file mode 100644 index 8dbae655..00000000 --- a/docs/_modules/easygraph/functions/community/ego_graph.html +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - easygraph.functions.community.ego_graph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.community.ego_graph

-__all__ = ["ego_graph"]
-
-# import easygraph as eg
-from easygraph.functions.path import single_source_dijkstra
-
-
-
[docs]def ego_graph(G, n, radius=1, center=True, undirected=False, distance=None): - """Returns induced subgraph of neighbors centered at node n within - a given radius. - - Parameters - ---------- - G : graph - A EasyGraph Graph or DiGraph - - n : node - A single node - - radius : number, optional - Include all neighbors of distance<=radius from n. - - center : bool, optional - If False, do not include center node in graph - - undirected : bool, optional - If True use both in- and out-neighbors of directed graphs. - - distance : key, optional - Use specified edge data key as distance. For example, setting - distance='weight' will use the edge weight to measure the - distance from the node n. - - Notes - ----- - For directed graphs D this produces the "out" neighborhood - or successors. If you want the neighborhood of predecessors - first reverse the graph with D.reverse(). If you want both - directions use the keyword argument undirected=True. - - Node, edge, and graph attributes are copied to the returned subgraph. - """ - if undirected: - """ - if distance is not None: - sp, _ = eg.single_source_dijkstra( - G.to_undirected(), n, cutoff=radius, weight=distance - ) - else: - sp = dict( - eg.single_source_shortest_path_length( - G.to_undirected(), n, cutoff=radius - ) - ) - """ - else: - if distance is not None: - sp = single_source_dijkstra(G, n, weight=distance) - else: - sp = single_source_dijkstra(G, n) - nodes = [key for key, value in sp.items() if value <= radius] - nodes = list(nodes) - - H = G.nodes_subgraph(nodes) - if not center: - H.remove_node(n) - return H
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/community/louvain.html b/docs/_modules/easygraph/functions/community/louvain.html deleted file mode 100644 index dbcaf49f..00000000 --- a/docs/_modules/easygraph/functions/community/louvain.html +++ /dev/null @@ -1,463 +0,0 @@ - - - - - - easygraph.functions.community.louvain — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.community.louvain

-from collections import defaultdict
-from collections import deque
-
-import easygraph as eg
-
-from easygraph.functions.community.modularity import *
-
-
-__all__ = ["louvain_communities", "louvain_partitions"]
-
-
-
[docs]def louvain_communities(G, weight="weight", threshold=0.00002): - r"""Find the best partition of a graph using the Louvain Community Detection - Algorithm. - - Louvain Community Detection Algorithm is a simple method to extract the community - structure of a network. This is a heuristic method based on modularity optimization. [1]_ - - The algorithm works in 2 steps. On the first step it assigns every node to be - in its own community and then for each node it tries to find the maximum positive - modularity gain by moving each node to all of its neighbor communities. If no positive - gain is achieved the node remains in its original community. - - The modularity gain obtained by moving an isolated node $i$ into a community $C$ can - easily be calculated by the following formula (combining [1]_ [2]_ and some algebra): - - .. math:: - \Delta Q = \frac{k_{i,in}}{2m} - \gamma\frac{ \Sigma_{tot} \cdot k_i}{2m^2} - - where $m$ is the size of the graph, $k_{i,in}$ is the sum of the weights of the links - from $i$ to nodes in $C$, $k_i$ is the sum of the weights of the links incident to node $i$, - $\Sigma_{tot}$ is the sum of the weights of the links incident to nodes in $C$ and $\gamma$ - is the resolution parameter. - - For the directed case the modularity gain can be computed using this formula according to [3]_ - - .. math:: - \Delta Q = \frac{k_{i,in}}{m} - - \gamma\frac{k_i^{out} \cdot\Sigma_{tot}^{in} + k_i^{in} \cdot \Sigma_{tot}^{out}}{m^2} - - where $k_i^{out}$, $k_i^{in}$ are the outer and inner weighted degrees of node $i$ and - $\Sigma_{tot}^{in}$, $\Sigma_{tot}^{out}$ are the sum of in-going and out-going links incident - to nodes in $C$. - - The first phase continues until no individual move can improve the modularity. - - The second phase consists in building a new network whose nodes are now the communities - found in the first phase. To do so, the weights of the links between the new nodes are given by - the sum of the weight of the links between nodes in the corresponding two communities. Once this - phase is complete it is possible to reapply the first phase creating bigger communities with - increased modularity. - - The above two phases are executed until no modularity gain is achieved (or is less than - the `threshold`). - - Parameters - ---------- - threshold - G : easygraph - weight : string or None, optional (default="weight") - The name of an edge attribute that holds the numerical value - used as a weight. If None then each edge has weight 1. - - Returns - ------- - list - A list of sets (partition of `G`). Each set represents one community and contains - all the nodes that constitute it. - - Notes - ----- - The order in which the nodes are considered can affect the final output. In the algorithm - the ordering happens using a random shuffle. - - References - ---------- - .. [1] Blondel, V.D. et al. Fast unfolding of communities in - large networks. J. Stat. Mech 10008, 1-12(2008). https://doi.org/10.1088/1742-5468/2008/10/P10008 - .. [2] Traag, V.A., Waltman, L. & van Eck, N.J. From Louvain to Leiden: guaranteeing - well-connected communities. Sci Rep 9, 5233 (2019). https://doi.org/10.1038/s41598-019-41695-z - .. [3] Nicolas Dugu��, Anthony Perez. Directed Louvain : maximizing modularity in directed networks. - [Research Report] Universit�� d��Orl��ans. 2015. hal-01231784. https://hal.archives-ouvertes.fr/hal-01231784 - - See Also - -------- - louvain_partitions - """ - d = louvain_partitions(G, weight, threshold) - q = deque(d, maxlen=1) - # q.append(d) - return q.pop()
- - -
[docs]def louvain_partitions(G, weight="weight", threshold=0.0000001): - """Yields partitions for each level of the Louvain Community Detection Algorithm - - Louvain Community Detection Algorithm is a simple method to extract the community - structure of a network. This is a heuristic method based on modularity optimization. [1]_ - - The partitions at each level (step of the algorithm) form a dendogram of communities. - A dendrogram is a diagram representing a tree and each level represents - a partition of the G graph. The top level contains the smallest communities - and as you traverse to the bottom of the tree the communities get bigger - and the overall modularity increases making the partition better. - - Each level is generated by executing the two phases of the Louvain Community - Detection Algorithm. - - Parameters - ---------- - threshold - G : easygraph - weight : string or None, optional (default="weight") - The name of an edge attribute that holds the numerical value - used as a weight. If None then each edge has weight 1. - - Yields - ------ - list - A list of sets (partition of `G`). Each set represents one community and contains - all the nodes that constitute it. - - References - ---------- - .. [1] Blondel, V.D. et al. Fast unfolding of communities in - large networks. J. Stat. Mech 10008, 1-12(2008) - - See Also - -------- - louvain_communities - """ - partition = [{u} for u in G.nodes] - mod = modularity(G, partition) - is_directed = G.is_directed() - if G.is_multigraph(): - G = _convert_multigraph(G, weight, is_directed) - else: - graph = G.__class__() - graph.add_nodes_from(G) - graph.add_edges_from(G.edges, weight=1) - G = graph - - m = G.size(weight="weight") - partition, inner_partition, improvement = _one_level(G, m, partition, is_directed) - improvement = True - while improvement: - # gh-5901 protect the sets in the yielded list from further manipulation here - - yield [s.copy() for s in partition] - new_mod = modularity(G, inner_partition, weight="weight") - if new_mod - mod <= threshold: - return - mod = new_mod - """ - for node1, node2, wt in G.edges: - print(node1,node2,wt) - print("\n") - """ - G = _gen_graph(G, inner_partition) - """ - for node1, node2, wt in G.edges: - print(node1,node2,wt) - """ - partition, inner_partition, improvement = _one_level( - G, m, partition, is_directed, 1 - )
- - -def _one_level(G, m, partition, resolution=1, is_directed=False, seed=None, tes=0): - """Calculate one level of the Louvain partitions tree - - Parameters - ---------- - G : EasyGraph Graph/DiGraph - The graph from which to detect communities - m : number - The size of the graph `G`. - partition : list of sets of nodes - A valid partition of the graph `G` - resolution : positive number - The resolution parameter for computing the modularity of a partition - is_directed : bool - True if `G` is a directed graph. - seed : integer, random_state, or None (default) - Indicator of random number generation state. - See :ref:`Randomness<randomness>`. - - """ - node2com = {u: i for i, u in enumerate(G.nodes)} - inner_partition = [{u} for u in G.nodes] - """ - if is_directed: - in_degrees = dict(G.in_degree(weight="weight")) - out_degrees = dict(G.out_degree(weight="weight")) - Stot_in = list(in_degrees.values()) - Stot_out = list(out_degrees.values()) - # Calculate weights for both in and out neighbours - nbrs = {} - for u in G: - nbrs[u] = defaultdict(float) - for _, n, wt in G.out_edges(u, data="weight"): - nbrs[u][n] += wt - for n, _, wt in G.in_edges(u, data="weight"): - nbrs[u][n] += wt - pass - else: - """ - degrees = dict(G.degree(weight="weight")) - Stot = [] - for i in G: - Stot.append(len(G[i])) - - # for c in Stot: - # print(c) - - nbrs = {u: {v: data["weight"] for v, data in G[u].items() if v != u} for u in G} - rand_nodes = list(G.nodes) - # seed.shuffle(rand_nodes) - nb_moves = 1 - improvement = False - while nb_moves > 0: - # print(nb_moves) - - nb_moves = 0 - for u in rand_nodes: - best_mod = 0 - best_com = node2com[u] - weights2com = _neighbor_weights(nbrs[u], node2com) - """ - if is_directed: - in_degree = in_degrees[u] - out_degree = out_degrees[u] - Stot_in[best_com] -= in_degree - Stot_out[best_com] -= out_degree - remove_cost = ( - -weights2com[best_com] / m - + (out_degree * Stot_in[best_com] + in_degree * Stot_out[best_com]) - / m**2 - ) - else: - """ - degree = degrees[u] - Stot[best_com] -= degree - remove_cost = -weights2com[best_com] / m + (Stot[best_com] * degree) / ( - 2 * m**2 - ) - for nbr_com, wt in weights2com.items(): - """ - if is_directed: - gain = ( - remove_cost - + wt / m - - ( - out_degree * Stot_in[nbr_com] - + in_degree * Stot_out[nbr_com] - ) - / m**2 - ) - else: - """ - gain = remove_cost + wt / m - (Stot[nbr_com] * degree) / (2 * m**2) - if gain > best_mod: - best_mod = gain - best_com = nbr_com - """ - if is_directed: - Stot_in[best_com] += in_degree - Stot_out[best_com] += out_degree - else: - """ - Stot[best_com] += degree - - if best_com != node2com[u]: - com = G.nodes[u].get("nodes", {u}) - partition[node2com[u]].difference_update(com) - inner_partition[node2com[u]].remove(u) - partition[best_com].update(com) - inner_partition[best_com].add(u) - improvement = True - nb_moves += 1 - node2com[u] = best_com - partition = list(filter(len, partition)) - inner_partition = list(filter(len, inner_partition)) - - # for c in partition: - # print(c) - - return partition, inner_partition, improvement - - -def _neighbor_weights(nbrs, node2com): - """Calculate weights between node and its neighbor communities. - - Parameters - ---------- - nbrs : dictionary - Dictionary with nodes' neighbours as keys and their edge weight as value. - node2com : dictionary - Dictionary with all graph's nodes as keys and their community index as value. - - """ - weights = defaultdict(float) - for nbr, wt in nbrs.items(): - weights[node2com[nbr]] += wt - return weights - - -def _gen_graph(G, partition): - """Generate a new graph based on the partitions of a given graph""" - H = G.__class__() - node2com = {} - for i, part in enumerate(partition): - nodes = set() - for node in part: - node2com[node] = i - nodes.update(G.nodes[node].get("nodes", {node})) - H.add_node(i, nodes=nodes) - - for node1, node2, wt in G.edges: - com1 = node2com[node1] - com2 = node2com[node2] - wt = wt["weight"] - try: - temp = H[com1][com2]["weight"] - except KeyError: - temp = 0 - H.add_edge(com1, com2, weight=wt + temp) - """ - if wt: - wt = wt["weight"] - H.add_edge(com1, com2, weight=wt) - else: - H.add_edge(com1, com2, weight=1) - """ - return H - - -def _convert_multigraph(G, weight, is_directed): - """Convert a Multigraph to normal Graph""" - if is_directed: - H = eg.DiGraph() - else: - H = eg.Graph() - H.add_nodes_from(G) - for u, v, wt in G.edges(data=weight, default=1): - if H.has_edge(u, v): - H[u][v]["weight"] += wt - else: - H.add_edge(u, v, weight=wt) - return H -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/community/modularity.html b/docs/_modules/easygraph/functions/community/modularity.html deleted file mode 100644 index f04d437e..00000000 --- a/docs/_modules/easygraph/functions/community/modularity.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - easygraph.functions.community.modularity — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.community.modularity

-from itertools import product
-
-from easygraph.utils import *
-
-
-__all__ = ["modularity"]
-
-
-
[docs]@not_implemented_for("multigraph") -def modularity(G, communities, weight="weight"): - r""" - Returns the modularity of the given partition of the graph. - Modularity is defined in [1]_ as - - .. math:: - - Q = \frac{1}{2m} \sum_{ij} \left( A_{ij} - \frac{k_ik_j}{2m}\right) - \delta(c_i,c_j) - - where m is the number of edges, A is the adjacency matrix of - `G`, - - .. math:: - - k_i\ is\ the\ degree\ of\ i\ and\ \delta(c_i, c_j)\ is\ 1\ if\ i\ and\ j\ are\ in\ the\ same\ community\ and\ 0\ otherwise. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - communities : list or iterable of set of nodes - These node sets must represent a partition of G's nodes. - - weight : string, optional (default : 'weight') - The key for edge weight. - - Returns - ---------- - Q : float - The modularity of the partition. - - References - ---------- - .. [1] M. E. J. Newman *Networks: An Introduction*, page 224. - Oxford University Press, 2011. - - """ - # TODO: multigraph not included. - - if not isinstance(communities, list): - communities = list(communities) - - directed = G.is_directed() - m = G.size(weight=weight) - if directed: - out_degree = dict(G.out_degree(weight=weight)) - in_degree = dict(G.in_degree(weight=weight)) - norm = 1 / m - else: - out_degree = dict(G.degree(weight=weight)) - in_degree = out_degree - norm = 1 / (2 * m) - - def val(u, v): - try: - w = G[u][v].get(weight, 1) - except KeyError: - w = 0 - # Double count self-loops if the graph is undirected. - if u == v and not directed: - w *= 2 - return w - in_degree[u] * out_degree[v] * norm - - Q = sum(val(u, v) for c in communities for u, v in product(c, repeat=2)) - return Q * norm
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/community/modularity_max_detection.html b/docs/_modules/easygraph/functions/community/modularity_max_detection.html deleted file mode 100644 index f13e3adb..00000000 --- a/docs/_modules/easygraph/functions/community/modularity_max_detection.html +++ /dev/null @@ -1,313 +0,0 @@ - - - - - - easygraph.functions.community.modularity_max_detection — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.community.modularity_max_detection

-from easygraph.functions.community.modularity import modularity
-from easygraph.utils import *
-from easygraph.utils.mapped_queue import MappedQueue
-
-
-__all__ = ["greedy_modularity_communities"]
-
-
-
[docs]@not_implemented_for("multigraph") -def greedy_modularity_communities(G, weight="weight"): - """Communities detection via greedy modularity method. - - Find communities in graph using Clauset-Newman-Moore greedy modularity - maximization. This method currently supports the Graph class. - - Greedy modularity maximization begins with each node in its own community - and joins the pair of communities that most increases modularity until no - such pair exists. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - weight : string (default : 'weight') - The key for edge weight. For undirected graph, it will regard each edge - weight as 1. - - Returns - ---------- - Yields sets of nodes, one for each community. - - References - ---------- - .. [1] Newman, M. E. J. "Networks: An Introduction Oxford Univ." (2010). - .. [2] Clauset, Aaron, Mark EJ Newman, and Cristopher Moore. - "Finding community structure in very large networks." Physical review E 70.6 (2004): 066111. - """ - - # Count nodes and edges - - N = len(G.nodes) - m = sum(d.get(weight, 1) for u, v, d in G.edges) - if N == 0 or m == 0: - print("Please input the graph which has at least one edge!") - exit() - q0 = 1.0 / (2.0 * m) - - # Map node labels to contiguous integers - label_for_node = {i: v for i, v in enumerate(G.nodes)} - node_for_label = {label_for_node[i]: i for i in range(N)} - - # Calculate degrees - k_for_label = G.degree(weight=weight) - k = [k_for_label[label_for_node[i]] for i in range(N)] - - # Initialize community and merge lists - communities = {i: frozenset([i]) for i in range(N)} - merges = [] - - # Initial modularity - partition = [[label_for_node[x] for x in c] for c in communities.values()] - q_cnm = modularity(G, partition) - - # Initialize data structures - # CNM Eq 8-9 (Eq 8 was missing a factor of 2 (from A_ij + A_ji) - # a[i]: fraction of edges within community i - # dq_dict[i][j]: dQ for merging community i, j - # dq_heap[i][n] : (-dq, i, j) for communitiy i nth largest dQ - # H[n]: (-dq, i, j) for community with nth largest max_j(dQ_ij) - a = [k[i] * q0 for i in range(N)] - dq_dict = { - i: { - j: 2 * q0 - 2 * k[i] * k[j] * q0 * q0 - for j in [node_for_label[u] for u in G.neighbors(label_for_node[i])] - if j != i - } - for i in range(N) - } - dq_heap = [ - MappedQueue([(-dq, i, j) for j, dq in dq_dict[i].items()]) for i in range(N) - ] - H = MappedQueue([dq_heap[i].h[0] for i in range(N) if len(dq_heap[i]) > 0]) - - # Merge communities until we can't improve modularity - while len(H) > 1: - # Find best merge - # Remove from heap of row maxes - # Ties will be broken by choosing the pair with lowest min community id - try: - dq, i, j = H.pop() - except IndexError: - break - dq = -dq - # Remove best merge from row i heap - dq_heap[i].pop() - # Push new row max onto H - if len(dq_heap[i]) > 0: - H.push(dq_heap[i].h[0]) - # If this element was also at the root of row j, we need to remove the - # duplicate entry from H - if dq_heap[j].h[0] == (-dq, j, i): - H.remove((-dq, j, i)) - # Remove best merge from row j heap - dq_heap[j].remove((-dq, j, i)) - # Push new row max onto H - if len(dq_heap[j]) > 0: - H.push(dq_heap[j].h[0]) - else: - # Duplicate wasn't in H, just remove from row j heap - dq_heap[j].remove((-dq, j, i)) - # Stop when change is non-positive - if dq <= 0: - break - - # Perform merge - communities[j] = frozenset(communities[i] | communities[j]) - del communities[i] - merges.append((i, j, dq)) - # New modularity - q_cnm += dq - # Get list of communities connected to merged communities - i_set = set(dq_dict[i].keys()) - j_set = set(dq_dict[j].keys()) - all_set = (i_set | j_set) - {i, j} - both_set = i_set & j_set - # Merge i into j and update dQ - for k in all_set: - # Calculate new dq value - if k in both_set: - dq_jk = dq_dict[j][k] + dq_dict[i][k] - elif k in j_set: - dq_jk = dq_dict[j][k] - 2.0 * a[i] * a[k] - else: - # k in i_set - dq_jk = dq_dict[i][k] - 2.0 * a[j] * a[k] - # Update rows j and k - for row, col in [(j, k), (k, j)]: - # Save old value for finding heap index - if k in j_set: - d_old = (-dq_dict[row][col], row, col) - else: - d_old = None - # Update dict for j,k only (i is removed below) - dq_dict[row][col] = dq_jk - # Save old max of per-row heap - if len(dq_heap[row]) > 0: - d_oldmax = dq_heap[row].h[0] - else: - d_oldmax = None - # Add/update heaps - d = (-dq_jk, row, col) - if d_old is None: - # We're creating a new nonzero element, add to heap - dq_heap[row].push(d) - else: - # Update existing element in per-row heap - dq_heap[row].update(d_old, d) - # Update heap of row maxes if necessary - if d_oldmax is None: - # No entries previously in this row, push new max - H.push(d) - else: - # We've updated an entry in this row, has the max changed? - if dq_heap[row].h[0] != d_oldmax: - H.update(d_oldmax, dq_heap[row].h[0]) - - # Remove row/col i from matrix - i_neighbors = dq_dict[i].keys() - for k in i_neighbors: - # Remove from dict - dq_old = dq_dict[k][i] - del dq_dict[k][i] - # Remove from heaps if we haven't already - if k != j: - # Remove both row and column - for row, col in [(k, i), (i, k)]: - # Check if replaced dq is row max - d_old = (-dq_old, row, col) - if dq_heap[row].h[0] == d_old: - # Update per-row heap and heap of row maxes - dq_heap[row].remove(d_old) - H.remove(d_old) - # Update row max - if len(dq_heap[row]) > 0: - H.push(dq_heap[row].h[0]) - else: - # Only update per-row heap - dq_heap[row].remove(d_old) - - del dq_dict[i] - # Mark row i as deleted, but keep placeholder - dq_heap[i] = MappedQueue() - # Merge i into j and update a - a[j] += a[i] - a[i] = 0 - - communities = [ - frozenset(label_for_node[i] for i in c) for c in communities.values() - ] - return sorted(communities, key=len, reverse=True)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/community/motif.html b/docs/_modules/easygraph/functions/community/motif.html deleted file mode 100644 index 5874ee2d..00000000 --- a/docs/_modules/easygraph/functions/community/motif.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - easygraph.functions.community.motif — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.community.motif

-import random
-
-import easygraph as eg
-
-from easygraph.utils import *
-
-
-__all__ = ["enumerate_subgraph", "random_enumerate_subgraph"]
-
-
-
[docs]@not_implemented_for("multigraph") -def enumerate_subgraph(G, k: int): - """ - Returns the motifs. - Motifs are small weakly connected induced subgraphs of a given structure in a graph. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph. - - k : int - The size of the motifs to search for. - - Returns - ---------- - k_subgraphs : list - The motifs. - - References - ---------- - .. [1] Wernicke, Sebastian. "Efficient detection of network motifs." - IEEE/ACM transactions on computational biology and bioinformatics 3.4 (2006): 347-359. - - """ - k_subgraphs = [] - for v, _ in G.nodes.items(): - Vextension = {u for u in G.adj[v] if u > v} - extend_subgraph(G, {v}, Vextension, v, k, k_subgraphs) - return k_subgraphs
- - -def extend_subgraph( - G, Vsubgraph: set, Vextension: set, v: int, k: int, k_subgraphs: list -): - if len(Vsubgraph) == k: - k_subgraphs.append(Vsubgraph) - return - while len(Vextension) > 0: - w = random.choice(tuple(Vextension)) - Vextension.remove(w) - NexclwVsubgraph = exclusive_neighborhood(G, w, Vsubgraph) - VpExtension = Vextension | {u for u in NexclwVsubgraph if u > v} - extend_subgraph(G, Vsubgraph | {w}, VpExtension, v, k, k_subgraphs) - - -def exclusive_neighborhood(G, v: int, vp: set): - Nv = set(G.adj[v]) - NVp = {u for n in vp for u in G.adj[n]} | vp - return Nv - NVp - - -
[docs]@not_implemented_for("multigraph") -def random_enumerate_subgraph(G, k: int, cut_prob: list): - """ - Returns the motifs. - Motifs are small weakly connected induced subgraphs of a given structure in a graph. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph. - - k : int - The size of the motifs to search for. - - cut_prob : list - list of probabilities for cutting the search tree at a given level. - - Returns - ---------- - k_subgraphs : list - The motifs. - - References - ---------- - .. [1] Wernicke, Sebastian. "A faster algorithm for detecting network motifs." - International Workshop on Algorithms in Bioinformatics. Springer, Berlin, Heidelberg, 2005. - - """ - if len(cut_prob) != k: - raise eg.EasyGraphError("length of cut_prob invalid, should equal to k") - - k_subgraphs = [] - for v, _ in G.nodes.items(): - if random.random() > cut_prob[0]: - continue - Vextension = {u for u in G.adj[v] if u > v} - random_extend_subgraph(G, {v}, Vextension, v, k, k_subgraphs, cut_prob) - return k_subgraphs
- - -def random_extend_subgraph( - G, - Vsubgraph: set, - Vextension: set, - v: int, - k: int, - k_subgraphs: list, - cut_prob: list, -): - if len(Vsubgraph) == k: - k_subgraphs.append(Vsubgraph) - return - while len(Vextension) > 0: - w = random.choice(tuple(Vextension)) - Vextension.remove(w) - NexclwVsubgraph = exclusive_neighborhood(G, w, Vsubgraph) - VpExtension = Vextension | {u for u in NexclwVsubgraph if u > v} - if random.random() > cut_prob[len(Vsubgraph)]: - continue - random_extend_subgraph(G, Vsubgraph | {w}, VpExtension, v, k, k_subgraphs) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/components/biconnected.html b/docs/_modules/easygraph/functions/components/biconnected.html deleted file mode 100644 index ed89338c..00000000 --- a/docs/_modules/easygraph/functions/components/biconnected.html +++ /dev/null @@ -1,360 +0,0 @@ - - - - - - easygraph.functions.components.biconnected — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.components.biconnected

-from itertools import chain
-
-from easygraph.utils import *
-
-
-__all__ = [
-    "is_biconnected",
-    "biconnected_components",
-    "generator_biconnected_components_nodes",
-    "generator_biconnected_components_edges",
-    "generator_articulation_points",
-]
-
-
-
[docs]@not_implemented_for("multigraph", "directed") -def is_biconnected(G): - """Returns whether the graph is biconnected or not. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - Returns - ------- - is_biconnected : boolean - `True` if the graph is biconnected. - - Examples - -------- - - >>> is_biconnected(G) - - """ - bc_nodes = list(generator_biconnected_components_nodes(G)) - if len(bc_nodes) == 1: - return len(bc_nodes[0]) == len( - G - ) # avoid situations where there is isolated vertex - return False
- - -
[docs]@not_implemented_for("multigraph", "directed") -# TODO: get the subgraph of each biconnected graph -def biconnected_components(G): - """Returns a list of biconnected components, each of which denotes the edges set of a biconnected component. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - Returns - ------- - biconnected_components : list of list - Each element list is the edges set of a biconnected component. - - Examples - -------- - >>> connected_components(G) - - """ - return list(generator_biconnected_components_edges(G))
- - -
[docs]@not_implemented_for("multigraph", "directed") -def generator_biconnected_components_nodes(G): - """Returns a generator of nodes in each biconnected component. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - Returns - ------- - Yields nodes set of each biconnected component. - - See Also - -------- - generator_biconnected_components_edges - - Examples - -------- - >>> generator_biconnected_components_nodes(G) - - - """ - for component in _biconnected_dfs_record_edges(G, need_components=True): - # TODO: only one edge = biconnected_component? - yield set(chain.from_iterable(component))
- - -
[docs]@not_implemented_for("multigraph", "directed") -def generator_biconnected_components_edges(G): - """Returns a generator of nodes in each biconnected component. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - Returns - ------- - Yields edges set of each biconnected component. - - See Also - -------- - generator_biconnected_components_nodes - - Examples - -------- - >>> generator_biconnected_components_edges(G) - - """ - yield from _biconnected_dfs_record_edges(G, need_components=True)
- - -
[docs]@not_implemented_for("multigraph", "directed") -def generator_articulation_points(G): - """Returns a generator of articulation points. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - Returns - ------- - Yields the articulation point in *G*. - - Examples - -------- - >>> generator_articulation_points(G) - - """ - seen = set() - for cut_vertex in _biconnected_dfs_record_edges(G, need_components=False): - if cut_vertex not in seen: - seen.add(cut_vertex) - yield cut_vertex
- - -@hybrid("cpp_biconnected_dfs_record_edges") -def _biconnected_dfs_record_edges(G, need_components=True): - """ - References - ---------- - https://www.cnblogs.com/nullzx/p/7968110.html - https://blog.csdn.net/gauss_acm/article/details/43493903 - """ - # record edges of each biconnected component in traversal - # Copied version from EasyGraph - # depth-first search algorithm to generate articulation points - # and biconnected components - visited = set() - for start in G: - if start in visited: - continue - discovery = {start: 0} # time of first discovery of node during search - low = {start: 0} - root_children = 0 - visited.add(start) - edge_stack = [] - stack = [(start, start, iter(G[start]))] - while stack: - grandparent, parent, children = stack[-1] - try: - child = next(children) - if grandparent == child: - continue - if child in visited: - if discovery[child] <= discovery[parent]: # back edge - low[parent] = min(low[parent], discovery[child]) - if need_components: - edge_stack.append((parent, child)) - else: - low[child] = discovery[child] = len(discovery) - visited.add(child) - stack.append((parent, child, iter(G[child]))) - if need_components: - edge_stack.append((parent, child)) - except StopIteration: - stack.pop() - if len(stack) > 1: - if low[parent] >= discovery[grandparent]: - if need_components: - ind = edge_stack.index((grandparent, parent)) - yield edge_stack[ind:] - edge_stack = edge_stack[:ind] - else: - yield grandparent - low[grandparent] = min(low[parent], low[grandparent]) - elif stack: # length 1 so grandparent is root - root_children += 1 - if need_components: - ind = edge_stack.index((grandparent, parent)) - yield edge_stack[ind:] - if not need_components: - # root node is articulation point if it has more than 1 child - if root_children > 1: - yield start - - -def _biconnected_dfs_record_nodes(G, need_components=True): - # record nodes of each biconnected component in traversal - # Not used. - visited = set() - for start in G: - if start in visited: - continue - discovery = {start: 0} # time of first discovery of node during search - low = {start: 0} - root_children = 0 - visited.add(start) - node_stack = [start] - stack = [(start, start, iter(G[start]))] - while stack: - grandparent, parent, children = stack[-1] - try: - child = next(children) - if grandparent == child: - continue - if child in visited: - if discovery[child] <= discovery[parent]: # back edge - low[parent] = min(low[parent], discovery[child]) - else: - low[child] = discovery[child] = len(discovery) - visited.add(child) - stack.append((parent, child, iter(G[child]))) - if need_components: - node_stack.append(child) - except StopIteration: - stack.pop() - if len(stack) > 1: - if low[parent] >= discovery[grandparent]: - if need_components: - ind = node_stack.index(grandparent) - yield node_stack[ind:] - node_stack = node_stack[: ind + 1] - else: - yield grandparent - low[grandparent] = min(low[parent], low[grandparent]) - elif stack: # length 1 so grandparent is root - root_children += 1 - if need_components: - ind = node_stack.index(grandparent) - yield node_stack[ind:] - if not need_components: - # root node is articulation point if it has more than 1 child - if root_children > 1: - yield start -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/components/connected.html b/docs/_modules/easygraph/functions/components/connected.html deleted file mode 100644 index 630c1dc3..00000000 --- a/docs/_modules/easygraph/functions/components/connected.html +++ /dev/null @@ -1,273 +0,0 @@ - - - - - - easygraph.functions.components.connected — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.components.connected

-from easygraph.utils.decorators import *
-
-
-__all__ = [
-    "is_connected",
-    "number_connected_components",
-    "connected_components",
-    "connected_components_directed",
-    "connected_component_of_node",
-]
-
-
-
[docs]@not_implemented_for("multigraph") -def is_connected(G): - """Returns whether the graph is connected or not. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - Returns - ------- - is_biconnected : boolean - `True` if the graph is connected. - - Examples - -------- - - >>> is_connected(G) - - """ - assert len(G) != 0, "No node in the graph." - arbitrary_node = next(iter(G)) # Pick an arbitrary node to run BFS - return len(G) == sum(1 for node in _plain_bfs(G, arbitrary_node))
- - -
[docs]@not_implemented_for("multigraph") -def number_connected_components(G): - """Returns the number of connected components. - - Parameters - ---------- - G : easygraph.Graph - - Returns - ------- - number_connected_components : int - The number of connected components. - - Examples - -------- - >>> number_connected_components(G) - - """ - return sum(1 for component in _generator_connected_components(G))
- - -@not_implemented_for("multigraph") -@hybrid("cpp_connected_components_undirected") -def connected_components(G): - """Returns a list of connected components, each of which denotes the edges set of a connected component. - - Parameters - ---------- - G : easygraph.Graph - Returns - ------- - connected_components : list of list - Each element list is the edges set of a connected component. - - Examples - -------- - >>> connected_components(G) - - """ - seen = set() - for v in G: - if v not in seen: - c = set(_plain_bfs(G, v)) - seen.update(c) - yield c - - -@not_implemented_for("multigraph") -@hybrid("cpp_connected_components_directed") -def connected_components_directed(G): - """Returns a list of connected components, each of which denotes the edges set of a connected component. - - Parameters - ---------- - G : easygraph.DiGraph - Returns - ------- - connected_components : list of list - Each element list is the edges set of a connected component. - - Examples - -------- - >>> connected_components(G) - - """ - seen = set() - for v in G: - if v not in seen: - c = set(_plain_bfs(G, v)) - seen.update(c) - yield c - - -def _generator_connected_components(G): - seen = set() - for v in G: - if v not in seen: - component = set(_plain_bfs(G, v)) - yield component - seen.update(component) - - -
[docs]@not_implemented_for("multigraph") -def connected_component_of_node(G, node): - """Returns the connected component that *node* belongs to. - - Parameters - ---------- - G : easygraph.Graph - - node : object - The target node - - Returns - ------- - connected_component_of_node : set - The connected component that *node* belongs to. - - Examples - -------- - Returns the connected component of one node `Jack`. - - >>> connected_component_of_node(G, node='Jack') - - """ - return set(_plain_bfs(G, node))
- - -@hybrid("cpp_plain_bfs") -def _plain_bfs(G, source): - """ - A fast BFS node generator - """ - G_adj = G.adj - seen = set() - nextlevel = {source} - while nextlevel: - thislevel = nextlevel - nextlevel = set() - for v in thislevel: - if v not in seen: - yield v - seen.add(v) - nextlevel.update(G_adj[v]) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/components/strongly_connected.html b/docs/_modules/easygraph/functions/components/strongly_connected.html deleted file mode 100644 index 7bc7d112..00000000 --- a/docs/_modules/easygraph/functions/components/strongly_connected.html +++ /dev/null @@ -1,357 +0,0 @@ - - - - - - easygraph.functions.components.strongly_connected — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.components.strongly_connected

-import easygraph as eg
-
-from easygraph.utils.decorators import *
-
-
-__all__ = [
-    "number_strongly_connected_components",
-    "strongly_connected_components",
-    "is_strongly_connected",
-    "condensation",
-]
-
-
-@not_implemented_for("undirected")
-@hybrid("cpp_strongly_connected_components")
-def strongly_connected_components(G):
-    """Generate nodes in strongly connected components of graph.
-
-    Parameters
-    ----------
-    G : EasyGraph Graph
-        A directed graph.
-
-    Returns
-    -------
-    comp : generator of sets
-        A generator of sets of nodes, one for each strongly connected
-        component of G.
-
-    Raises
-    ------
-    EasyGraphNotImplemented
-        If G is undirected.
-
-    Examples
-    --------
-    Generate a sorted list of strongly connected components, largest first.
-
-    If you only want the largest component, it's more efficient to
-    use max instead of sort.
-
-    >>> largest = max(eg.strongly_connected_components(G), key=len)
-
-    See Also
-    --------
-    connected_components
-
-    Notes
-    -----
-    Uses Tarjan's algorithm[1]_ with Nuutila's modifications[2]_.
-    Nonrecursive version of algorithm.
-
-    References
-    ----------
-    .. [1] Depth-first search and linear graph algorithms, R. Tarjan
-       SIAM Journal of Computing 1(2):146-160, (1972).
-
-    .. [2] On finding the strongly connected components in a directed graph.
-       E. Nuutila and E. Soisalon-Soinen
-       Information Processing Letters 49(1): 9-14, (1994)..
-
-    """
-    preorder = {}
-    lowlink = {}
-    scc_found = set()
-    scc_queue = []
-    i = 0  # Preorder counter
-    neighbors = {v: iter(G[v]) for v in G}
-    for source in G:
-        if source not in scc_found:
-            queue = [source]
-            while queue:
-                v = queue[-1]
-                if v not in preorder:
-                    i = i + 1
-                    preorder[v] = i
-                done = True
-                for w in neighbors[v]:
-                    if w not in preorder:
-                        queue.append(w)
-                        done = False
-                        break
-                if done:
-                    lowlink[v] = preorder[v]
-                    for w in G[v]:
-                        if w not in scc_found:
-                            if preorder[w] > preorder[v]:
-                                lowlink[v] = min([lowlink[v], lowlink[w]])
-                            else:
-                                lowlink[v] = min([lowlink[v], preorder[w]])
-                    queue.pop()
-                    if lowlink[v] == preorder[v]:
-                        scc = {v}
-                        while scc_queue and preorder[scc_queue[-1]] > preorder[v]:
-                            k = scc_queue.pop()
-                            scc.add(k)
-                        scc_found.update(scc)
-                        yield scc
-                    else:
-                        scc_queue.append(v)
-
-
-
[docs]def number_strongly_connected_components(G): - """Returns number of strongly connected components in graph. - - Parameters - ---------- - G : Easygraph graph - A directed graph. - - Returns - ------- - n : integer - Number of strongly connected components - - Raises - ------ - EasygraphNotImplemented - If G is undirected. - - Examples - -------- - >>> G = eg.DiGraph([(0, 1), (1, 2), (2, 0), (2, 3), (4, 5), (3, 4), (5, 6), (6, 3), (6, 7)]) - >>> eg.number_strongly_connected_components(G) - 3 - - See Also - -------- - strongly_connected_components - number_connected_components - - Notes - ----- - For directed graphs only. - """ - return sum(1 for scc in strongly_connected_components(G))
- - -
[docs]@not_implemented_for("undirected") -def is_strongly_connected(G): - """Test directed graph for strong connectivity. - - A directed graph is strongly connected if and only if every vertex in - the graph is reachable from every other vertex. - - Parameters - ---------- - G : EasyGraph Graph - A directed graph. - - Returns - ------- - connected : bool - True if the graph is strongly connected, False otherwise. - - Examples - -------- - >>> G = eg.DiGraph([(0, 1), (1, 2), (2, 3), (3, 0), (2, 4), (4, 2)]) - >>> eg.is_strongly_connected(G) - True - >>> G.remove_edge(2, 3) - >>> eg.is_strongly_connected(G) - False - - Raises - ------ - EasyGraphNotImplemented - If G is undirected. - - See Also - -------- - is_connected - is_biconnected - strongly_connected_components - - Notes - ----- - For directed graphs only. - """ - if len(G) == 0: - raise eg.EasyGraphPointlessConcept( - """Connectivity is undefined for the null graph.""" - ) - - return len(next(strongly_connected_components(G))) == len(G)
- - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_Directed_graph -def condensation(G, scc=None): - """Returns the condensation of G. - The condensation of G is the graph with each of the strongly connected - components contracted into a single node. - Parameters - ---------- - G : easygraph.DiGraph - A directed graph. - scc: list or generator (optional, default=None) - Strongly connected components. If provided, the elements in - `scc` must partition the nodes in `G`. If not provided, it will be - calculated as scc=strongly_connected_components(G). - Returns - ------- - C : easygraph.DiGraph - The condensation graph C of G. The node labels are integers - corresponding to the index of the component in the list of - strongly connected components of G. C has a graph attribute named - 'mapping' with a dictionary mapping the original nodes to the - nodes in C to which they belong. Each node in C also has a node - attribute 'members' with the set of original nodes in G that - form the SCC that the node in C represents. - Examples - -------- - # >>> condensation(G) - Notes - ----- - After contracting all strongly connected components to a single node, - the resulting graph is a directed acyclic graph. - """ - if scc is None: - scc = strongly_connected_components(G) - mapping = {} - incoming_info = {} - members = {} - C = eg.DiGraph() - # Add mapping dict as graph attribute - C.graph["mapping"] = mapping - if len(G) == 0: - return C - for i, component in enumerate(scc): - members[i] = component - mapping.update((n, i) for n in component) - number_of_components = i + 1 - for i in range(number_of_components): - C.add_node(i, member=members[i], incoming=set()) - C.add_nodes(range(number_of_components)) - for edge in G.edges: - if mapping[edge[0]] != mapping[edge[1]]: - C.add_edge(mapping[edge[0]], mapping[edge[1]]) - if edge[1] not in incoming_info.keys(): - incoming_info[edge[1]] = set() - incoming_info[edge[1]].add(edge[0]) - C.graph["incoming_info"] = incoming_info - return C
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/components/weakly_connected.html b/docs/_modules/easygraph/functions/components/weakly_connected.html deleted file mode 100644 index cea3402c..00000000 --- a/docs/_modules/easygraph/functions/components/weakly_connected.html +++ /dev/null @@ -1,299 +0,0 @@ - - - - - - easygraph.functions.components.weakly_connected — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.components.weakly_connected

-"""Weakly connected components."""
-import easygraph as eg
-
-from easygraph.utils.decorators import not_implemented_for
-
-
-__all__ = [
-    "number_weakly_connected_components",
-    "weakly_connected_components",
-    "is_weakly_connected",
-]
-
-
-
[docs]@not_implemented_for("undirected") -def weakly_connected_components(G): - """Generate weakly connected components of G. - - Parameters - ---------- - G : EasyGraph graph - A directed graph - - Returns - ------- - comp : generator of sets - A generator of sets of nodes, one for each weakly connected - component of G. - - Raises - ------ - EasyGraphNotImplemented - If G is undirected. - - Examples - -------- - Generate a sorted list of weakly connected components, largest first. - - >>> G = eg.path_graph(4, create_using=eg.DiGraph()) - >>> eg.add_path(G, [10, 11, 12]) - >>> [ - ... len(c) - ... for c in sorted(eg.weakly_connected_components(G), key=len, reverse=True) - ... ] - [4, 3] - - If you only want the largest component, it's more efficient to - use max instead of sort: - - >>> largest_cc = max(eg.weakly_connected_components(G), key=len) - - See Also - -------- - connected_components - strongly_connected_components - - Notes - ----- - For directed graphs only. - - """ - seen = set() - for v in G: - if v not in seen: - c = set(_plain_bfs(G, v)) - seen.update(c) - yield c
- - -
[docs]@not_implemented_for("undirected") -def number_weakly_connected_components(G): - """Returns the number of weakly connected components in G. - - Parameters - ---------- - G : EasyGraph graph - A directed graph. - - Returns - ------- - n : integer - Number of weakly connected components - - Raises - ------ - EasyGraphNotImplemented - If G is undirected. - - Examples - -------- - >>> G = eg.DiGraph([(0, 1), (2, 1), (3, 4)]) - >>> eg.number_weakly_connected_components(G) - 2 - - See Also - -------- - weakly_connected_components - number_connected_components - number_strongly_connected_components - - Notes - ----- - For directed graphs only. - - """ - return sum(1 for wcc in weakly_connected_components(G))
- - -
[docs]@not_implemented_for("undirected") -def is_weakly_connected(G): - """Test directed graph for weak connectivity. - - A directed graph is weakly connected if and only if the graph - is connected when the direction of the edge between nodes is ignored. - - Note that if a graph is strongly connected (i.e. the graph is connected - even when we account for directionality), it is by definition weakly - connected as well. - - Parameters - ---------- - G : EasyGraph Graph - A directed graph. - - Returns - ------- - connected : bool - True if the graph is weakly connected, False otherwise. - - Raises - ------ - EasyGraphNotImplemented - If G is undirected. - - Examples - -------- - >>> G = eg.DiGraph([(0, 1), (2, 1)]) - >>> G.add_node(3) - >>> eg.is_weakly_connected(G) # node 3 is not connected to the graph - False - >>> G.add_edge(2, 3) - >>> eg.is_weakly_connected(G) - True - - See Also - -------- - is_strongly_connected - is_semiconnected - is_connected - is_biconnected - weakly_connected_components - - Notes - ----- - For directed graphs only. - - """ - if len(G) == 0: - raise eg.EasyGraphPointlessConcept( - """Connectivity is undefined for the null graph.""" - ) - - return len(next(weakly_connected_components(G))) == len(G)
- - -def _plain_bfs(G, source): - """A fast BFS node generator - - The direction of the edge between nodes is ignored. - - For directed graphs only. - - """ - Gsucc = G.adj - Gpred = G.pred - - seen = set() - nextlevel = {source} - while nextlevel: - thislevel = nextlevel - nextlevel = set() - for v in thislevel: - if v not in seen: - seen.add(v) - nextlevel.update(Gsucc[v]) - nextlevel.update(Gpred[v]) - yield v -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/core/k_core.html b/docs/_modules/easygraph/functions/core/k_core.html deleted file mode 100644 index 246a3511..00000000 --- a/docs/_modules/easygraph/functions/core/k_core.html +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - easygraph.functions.core.k_core — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.core.k_core

-import easygraph as eg
-
-from easygraph.utils import *
-
-
-__all__ = [
-    "k_core",
-]
-
-
-from typing import TYPE_CHECKING
-from typing import List
-from typing import Union
-
-
-if TYPE_CHECKING:
-    from easygraph import Graph
-
-
-
[docs]@hybrid("cpp_k_core") -def k_core(G: "Graph") -> Union["Graph", List]: - """ - Returns the k-core of G. - - A k-core is a maximal subgraph that contains nodes of degree k or more. - - Parameters - ---------- - G : EasyGraph graph - A graph or directed graph - k : int, optional - The order of the core. If not specified return the main core. - return_graph : bool, optional - If True, return the k-core as a graph. If False, return a list of nodes. - - Returns - ------- - G : EasyGraph graph, if return_graph is True, else a list of nodes - The k-core subgraph - """ - # Create a shallow copy of the input graph - H = G.copy() - - # Initialize a dictionary to store the degrees of the nodes - degrees = dict(G.degree()) - # Sort nodes by degree. - nodes = sorted(degrees, key=degrees.get) - bin_boundaries = [0] - curr_degree = 0 - for i, v in enumerate(nodes): - if degrees[v] > curr_degree: - bin_boundaries.extend([i] * (degrees[v] - curr_degree)) - curr_degree = degrees[v] - node_pos = {v: pos for pos, v in enumerate(nodes)} - # The initial guess for the core number of a node is its degree. - core = degrees - nbrs = {v: list(G.neighbors(v)) for v in G} - for v in nodes: - for u in nbrs[v]: - if core[u] > core[v]: - nbrs[u].remove(v) - pos = node_pos[u] - bin_start = bin_boundaries[core[u]] - node_pos[u] = bin_start - node_pos[nodes[bin_start]] = pos - nodes[bin_start], nodes[pos] = nodes[pos], nodes[bin_start] - bin_boundaries[core[u]] += 1 - core[u] -= 1 - ret = [0.0 for i in range(len(G))] - for i in range(len(ret)): - ret[i] = core[G.index2node[i]] - return ret
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/drawing/defaults.html b/docs/_modules/easygraph/functions/drawing/defaults.html deleted file mode 100644 index d754f27a..00000000 --- a/docs/_modules/easygraph/functions/drawing/defaults.html +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - easygraph.functions.drawing.defaults — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.drawing.defaults

-from typing import Any
-from typing import List
-from typing import Optional
-from typing import Union
-
-
-
[docs]def default_style( - num_v: int, - num_e: int, - v_color: Union[str, list] = "r", - e_color: Union[str, list] = "gray", - e_fill_color: Union[str, list] = "whitesmoke", -): - _v_color = "r" - _e_color = "gray" - _e_fill_color = "whitesmoke" - - v_color = fill_color(v_color, _v_color, num_v) - e_color = fill_color(e_color, _e_color, num_e) - e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e) - - return v_color, e_color, e_fill_color
- - -
[docs]def default_bipartite_style( - num_u: int, - num_v: int, - num_e: int, - u_color: Union[str, list] = "m", - v_color: Union[str, list] = "r", - e_color: Union[str, list] = "gray", - e_fill_color: Union[str, list] = "whitesmoke", -): - _u_color = "m" - _v_color = "r" - _e_color = "gray" - _e_fill_color = "whitesmoke" - - u_color = fill_color(u_color, _u_color, num_u) - v_color = fill_color(v_color, _v_color, num_v) - e_color = fill_color(e_color, _e_color, num_e) - e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e) - - return u_color, v_color, e_color, e_fill_color
- - -
[docs]def default_hypergraph_style( - num_v: int, - num_e: int, - v_color: Union[str, list] = "r", - e_color: Union[str, list] = "gray", - e_fill_color: Union[str, list] = "whitesmoke", -): - _v_color = "r" - _e_color = "gray" - _e_fill_color = "whitesmoke" - - v_color = fill_color(v_color, _v_color, num_v) - e_color = fill_color(e_color, _e_color, num_e) - e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e) - - return v_color, e_color, e_fill_color
- - -
[docs]def default_size( - num_v: int, - e_list: List[tuple], - v_size: Union[float, list] = 1.0, - v_line_width: Union[float, list] = 1.0, - e_line_width: Union[float, list] = 1.0, - font_size: float = 1.0, -): - import numpy as np - - _v_size = 1 / np.sqrt(num_v + 10) * 0.1 - _v_line_width = 1 * np.exp(-num_v / 50) - _e_line_width = 1 * np.exp(-len(e_list) / 120) - _font_size = 20 * np.exp(-num_v / 100) - - v_size = fill_sizes(v_size, _v_size, num_v) - v_line_width = fill_sizes(v_line_width, _v_line_width, num_v) - e_line_width = fill_sizes(e_line_width, _e_line_width, len(e_list)) - font_size = _font_size if font_size is None else font_size * _font_size - - return v_size, v_line_width, e_line_width, font_size
- - -
[docs]def default_bipartite_size( - num_u: int, - num_v: int, - e_list: List[tuple], - u_size: Union[float, list] = 1.0, - u_line_width: Union[float, list] = 1.0, - v_size: Union[float, list] = 1.0, - v_line_width: Union[float, list] = 1.0, - e_line_width: Union[float, list] = 1.0, - u_font_size: float = 1.0, - v_font_size: float = 1.0, -): - import numpy as np - - _u_size = 1 / np.sqrt(num_u + 12) * 0.08 - _u_line_width = 1 * np.exp(-num_u / 50) - _v_size = 1 / np.sqrt(num_v + 12) * 0.08 - _v_line_width = 1 * np.exp(-num_v / 50) - _e_line_width = 1 * np.exp(-len(e_list) / 50) - _u_font_size = 12 * np.exp(-((num_u / num_v) ** 0.3) * (num_u + num_v) / 100) - _v_font_size = 12 * np.exp(-((num_v / num_u) ** 0.3) * (num_u + num_v) / 100) - - u_size = fill_sizes(u_size, _u_size, num_u) - u_line_width = fill_sizes(u_line_width, _u_line_width, num_u) - v_size = fill_sizes(v_size, _v_size, num_v) - v_line_width = fill_sizes(v_line_width, _v_line_width, num_v) - e_line_width = fill_sizes(e_line_width, _e_line_width, len(e_list)) - - u_font_size = _u_font_size if u_font_size is None else u_font_size * _u_font_size - v_font_size = _v_font_size if v_font_size is None else v_font_size * _v_font_size - - return ( - u_size, - u_line_width, - v_size, - v_line_width, - e_line_width, - u_font_size, - v_font_size, - )
- - -
[docs]def default_strength( - num_v: int, - e_list: List[tuple], - push_v_strength: float = 1.0, - push_e_strength: float = 1.0, - pull_e_strength: float = 1.0, - pull_center_strength: float = 1.0, -): - _push_v_strength = 0.006 - _push_e_strength = 0.0 - _pull_e_strength = 0.045 - _pull_center_strength = 0.01 - - push_v_strength = fill_strength(push_v_strength, _push_v_strength) - push_e_strength = fill_strength(push_e_strength, _push_e_strength) - pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength) - pull_center_strength = fill_strength(pull_center_strength, _pull_center_strength) - - return push_v_strength, push_e_strength, pull_e_strength, pull_center_strength
- - -
[docs]def default_bipartite_strength( - num_u: int, - num_v: int, - e_list: List[tuple], - push_u_strength: float = 1.0, - push_v_strength: float = 1.0, - push_e_strength: float = 1.0, - pull_e_strength: float = 1.0, - pull_u_center_strength: float = 1.0, - pull_v_center_strength: float = 1.0, -): - _push_u_strength = 0.005 - _push_v_strength = 0.005 - _push_e_strength = 0.0 - _pull_e_strength = 0.03 - _pull_u_center_strength = 0.04 - _pull_v_center_strength = 0.04 - - push_u_strength = fill_strength(push_u_strength, _push_u_strength) - push_v_strength = fill_strength(push_v_strength, _push_v_strength) - push_e_strength = fill_strength(push_e_strength, _push_e_strength) - pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength) - pull_u_center_strength = fill_strength( - pull_u_center_strength, _pull_u_center_strength - ) - pull_v_center_strength = fill_strength( - pull_v_center_strength, _pull_v_center_strength - ) - - return ( - push_u_strength, - push_v_strength, - push_e_strength, - pull_e_strength, - pull_u_center_strength, - pull_v_center_strength, - )
- - -
[docs]def default_hypergraph_strength( - num_v: int, - e_list: List[tuple], - push_v_strength: float = 1.0, - push_e_strength: float = 1.0, - pull_e_strength: float = 1.0, - pull_center_strength: float = 1.0, -): - _push_v_strength = 0.006 - _push_e_strength = 0.008 - _pull_e_strength = 0.007 - _pull_center_strength = 0.001 - - push_v_strength = fill_strength(push_v_strength, _push_v_strength) - push_e_strength = fill_strength(push_e_strength, _push_e_strength) - pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength) - pull_center_strength = fill_strength(pull_center_strength, _pull_center_strength) - - return push_v_strength, push_e_strength, pull_e_strength, pull_center_strength
- - -
[docs]def fill_color( - custom_color: Optional[Union[str, list]], default_color: Any, length: int -): - if custom_color is None: - return [default_color] * length - elif isinstance(custom_color, list): - if ( - isinstance(custom_color[0], str) - or isinstance(custom_color[0], tuple) - or isinstance(custom_color[0], list) - ): - return custom_color - else: - return [custom_color] * length - elif isinstance(custom_color, str): - return [custom_color] * length - else: - raise ValueError("The specified value is not a valid type.")
- - -
[docs]def fill_sizes( - custom_scales: Optional[Union[float, list]], default_value: Any, length: int -): - if custom_scales is None: - return [default_value] * length - elif isinstance(custom_scales, list): - assert ( - len(custom_scales) == length - ), "The specified value list has the wrong length." - return [default_value * scale for scale in custom_scales] - elif isinstance(custom_scales, float): - return [default_value * custom_scales] * length - elif isinstance(custom_scales, int): - return [default_value * float(custom_scales)] * length - else: - raise ValueError("The specified value is not a valid type.")
- - -
[docs]def fill_strength(custom_scale: Optional[float], default_value: float): - if custom_scale is None: - return default_value - return custom_scale * default_value
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/drawing/drawing.html b/docs/_modules/easygraph/functions/drawing/drawing.html deleted file mode 100644 index 14e85cde..00000000 --- a/docs/_modules/easygraph/functions/drawing/drawing.html +++ /dev/null @@ -1,714 +0,0 @@ - - - - - - easygraph.functions.drawing.drawing — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.drawing.drawing

-import random
-
-from copy import deepcopy
-from typing import List
-from typing import Optional
-from typing import Union
-
-import easygraph as eg
-
-
-__all__ = [
-    "draw_SHS_center",
-    "draw_SHS_center_kk",
-    "draw_kamada_kawai",
-    "draw_hypergraph",
-]
-
-from easygraph.functions.drawing.defaults import default_hypergraph_strength
-from easygraph.functions.drawing.defaults import default_hypergraph_style
-from easygraph.functions.drawing.defaults import default_size
-from easygraph.functions.drawing.layout import force_layout
-from easygraph.functions.drawing.utils import draw_circle_edge
-from easygraph.functions.drawing.utils import draw_vertex
-
-
-
[docs]def draw_hypergraph( - hg: "eg.Hypergraph", - e_style: str = "circle", - v_label: Optional[List[str]] = None, - v_size: Union[float, list] = 1.0, - v_color: Union[str, list] = "r", - v_line_width: Union[str, list] = 1.0, - e_color: Union[str, list] = "gray", - e_fill_color: Union[str, list] = "whitesmoke", - e_line_width: Union[str, list] = 1.0, - font_size: float = 1.0, - font_family: str = "sans-serif", - push_v_strength: float = 1.0, - push_e_strength: float = 1.0, - pull_e_strength: float = 1.0, - pull_center_strength: float = 1.0, -): - r"""Draw the hypergraph structure. - - Args: - ``hg`` (``eg.Hypergraph``): The EasyGraph's hypergraph object. - ``e_style`` (``str``): The style of hyperedges. The available styles are only ``'circle'``. Defaults to ``'circle'``. - ``v_label`` (``list``): The labels of vertices. Defaults to ``None``. - ``v_size`` (``float`` or ``list``): The size of vertices. Defaults to ``1.0``. - ``v_color`` (``str`` or ``list``): The `color <https://matplotlib.org/stable/gallery/color/named_colors.html>`_ of vertices. Defaults to ``'r'``. - ``v_line_width`` (``float`` or ``list``): The line width of vertices. Defaults to ``1.0``. - ``e_color`` (``str`` or ``list``): The `color <https://matplotlib.org/stable/gallery/color/named_colors.html>`_ of hyperedges. Defaults to ``'gray'``. - ``e_fill_color`` (``str`` or ``list``): The fill `color <https://matplotlib.org/stable/gallery/color/named_colors.html>`_ of hyperedges. Defaults to ``'whitesmoke'``. - ``e_line_width`` (``float`` or ``list``): The line width of hyperedges. Defaults to ``1.0``. - ``font_size`` (``float``): The font size of labels. Defaults to ``1.0``. - ``font_family`` (``str``): The font family of labels. Defaults to ``'sans-serif'``. - ``push_v_strength`` (``float``): The strength of pushing vertices. Defaults to ``1.0``. - ``push_e_strength`` (``float``): The strength of pushing hyperedges. Defaults to ``1.0``. - ``pull_e_strength`` (``float``): The strength of pulling hyperedges. Defaults to ``1.0``. - ``pull_center_strength`` (``float``): The strength of pulling vertices to the center. Defaults to ``1.0``. - """ - import matplotlib.pyplot as plt - - assert isinstance( - hg, eg.Hypergraph - ), "The input object must be a DHG's hypergraph object." - assert e_style in ["circle"], "e_style must be 'circle'" - assert hg.num_e > 0, "g must be a non-empty structure" - fig, ax = plt.subplots(figsize=(6, 6)) - - num_v, e_list = hg.num_v, deepcopy(hg.e[0]) - # default configures - v_color, e_color, e_fill_color = default_hypergraph_style( - hg.num_v, hg.num_e, v_color, e_color, e_fill_color - ) - v_size, v_line_width, e_line_width, font_size = default_size( - num_v, e_list, v_size, v_line_width, e_line_width - ) - ( - push_v_strength, - push_e_strength, - pull_e_strength, - pull_center_strength, - ) = default_hypergraph_strength( - num_v, - e_list, - push_v_strength, - push_e_strength, - pull_e_strength, - pull_center_strength, - ) - # layout - v_coor = force_layout( - num_v, - e_list, - push_v_strength, - push_e_strength, - pull_e_strength, - pull_center_strength, - ) - if e_style == "circle": - draw_circle_edge( - ax, - v_coor, - v_size, - e_list, - e_color, - e_fill_color, - e_line_width, - ) - else: - raise ValueError("e_style must be 'circle'") - - draw_vertex( - ax, - v_coor, - v_label, - font_size, - font_family, - v_size, - v_color, - v_line_width, - ) - - plt.xlim((0, 1.0)) - plt.ylim((0, 1.0)) - plt.axis("off") - fig.tight_layout() - plt.show()
- - -
[docs]def draw_SHS_center(G, SHS, rate=1, style="side"): - """ - Draw the graph whose the SH Spanners are in the center, with random layout. - - Parameters - ---------- - G : graph - A easygraph graph. - - SHS : list - The SH Spanners in graph G. - - rate : float - The proportion of visible points and edges to the total - - style : string - "side"- the label is next to the dot - "center"- the label is in the center of the dot - - Returns - ------- - graph : network - the graph whose the SH Spanners are in the center. - """ - import matplotlib.pyplot as plt - import numpy as np - - pos = eg.random_position(G) - center = np.zeros((len(SHS), 2), float) - node = np.zeros((len(pos) - len(SHS), 2), float) - m, n = 0, 0 - if rate == 1: - for i in pos: - if i in SHS: - center[n][0] = 0.5 + (-1) ** random.randint(1, 2) * pos[i][0] / 5 - center[n][1] = 0.5 + (-1) ** random.randint(1, 2) * pos[i][1] / 5 - pos[i][0] = center[n][0] - pos[i][1] = center[n][1] - n += 1 - else: - node[m][0] = pos[i][0] - node[m][1] = pos[i][1] - m += 1 - if style == "side": - plt.scatter(node[:, 0], node[:, 1], marker=".", color="b", s=10) - plt.scatter(center[:, 0], center[:, 1], marker="*", color="r", s=20) - elif style == "center": - plt.scatter( - node[:, 0], - node[:, 1], - marker="o", - color="None", - edgecolors="b", - s=50, - linewidth=0.5, - ) - plt.scatter( - center[:, 0], - center[:, 1], - marker="o", - color="None", - edgecolors="r", - s=50, - linewidth=0.5, - ) - k = 0 - for i in pos: - if style == "side": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="top", - horizontalalignment="right", - ) - elif style == "center": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="center", - horizontalalignment="center", - ) - k += 1 - for i in G.edges: - p1 = [pos[i[0]][0], pos[i[1]][0]] - p2 = [pos[i[0]][1], pos[i[1]][1]] - plt.plot(p1, p2, "k-", alpha=0.3, linewidth=0.5) - plt.show() - - else: - degree = G.degree() - sorted_degree = sorted(degree.items(), key=lambda d: d[1], reverse=True) - l = int(rate * len(G)) - s = [] - for i in sorted_degree: - if len(s) < l: - s.append(i[0]) - for i in pos: - if i in SHS and i in s: - center[n][0] = 0.5 + (-1) ** random.randint(1, 2) * pos[i][0] / 5 - center[n][1] = 0.5 + (-1) ** random.randint(1, 2) * pos[i][1] / 5 - pos[i][0] = center[n][0] - pos[i][1] = center[n][1] - n += 1 - elif i in s: - node[m][0] = pos[i][0] - node[m][1] = pos[i][1] - m += 1 - node = node[0:m, :] - center = center[0:n, :] - if style == "side": - plt.scatter(node[:, 0], node[:, 1], marker=".", color="b", s=10) - plt.scatter(center[:, 0], center[:, 1], marker="*", color="r", s=20) - elif style == "center": - plt.scatter( - node[:, 0], - node[:, 1], - marker="o", - color="None", - edgecolors="b", - s=50, - linewidth=0.5, - ) - plt.scatter( - center[:, 0], - center[:, 1], - marker="o", - color="None", - edgecolors="r", - s=50, - linewidth=0.5, - ) - k = 0 - for i in pos: - if i in s: - if style == "side": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="top", - horizontalalignment="right", - ) - elif style == "center": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="center", - horizontalalignment="center", - ) - k += 1 - for i in G.edges: - (u, v, t) = i - if u in s and v in s: - p1 = [pos[i[0]][0], pos[i[1]][0]] - p2 = [pos[i[0]][1], pos[i[1]][1]] - plt.plot(p1, p2, "k-", alpha=0.3, linewidth=0.5) - plt.show() - return
- - -
[docs]def draw_SHS_center_kk(G, SHS, rate=1, style="side"): - """ - Draw the graph whose the SH Spanners are in the center, with a Kamada-Kawai force-directed layout. - - Parameters - ---------- - G : graph - A easygraph graph. - - SHS : list - The SH Spanners in graph G. - - rate : float - The proportion of visible points and edges to the total - - style : string - "side"- the label is next to the dot - "center"- the label is in the center of the dot - - Returns - ------- - graph : network - the graph whose the SH Spanners are in the center. - """ - import matplotlib.pyplot as plt - import numpy as np - - pos = eg.kamada_kawai_layout(G) - center = np.zeros((len(SHS), 2), float) - node = np.zeros((len(pos) - len(SHS), 2), float) - m, n = 0, 0 - if rate == 1: - for i in pos: - if i in SHS: - center[n][0] = pos[i][0] / 5 - center[n][1] = pos[i][1] / 5 - pos[i][0] = center[n][0] - pos[i][1] = center[n][1] - n += 1 - else: - node[m][0] = pos[i][0] - node[m][1] = pos[i][1] - m += 1 - if style == "side": - plt.scatter(node[:, 0], node[:, 1], marker=".", color="b", s=10) - plt.scatter(center[:, 0], center[:, 1], marker="*", color="r", s=20) - elif style == "center": - plt.scatter( - node[:, 0], - node[:, 1], - marker="o", - color="None", - edgecolors="b", - s=50, - linewidth=0.5, - ) - plt.scatter( - center[:, 0], - center[:, 1], - marker="o", - color="None", - edgecolors="r", - s=50, - linewidth=0.5, - ) - k = 0 - for i in pos: - if style == "side": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="top", - horizontalalignment="right", - ) - elif style == "center": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="center", - horizontalalignment="center", - ) - k += 1 - for i in G.edges: - p1 = [pos[i[0]][0], pos[i[1]][0]] - p2 = [pos[i[0]][1], pos[i[1]][1]] - plt.plot(p1, p2, "k-", alpha=0.3, linewidth=0.5) - plt.show() - else: - degree = G.degree() - sorted_degree = sorted(degree.items(), key=lambda d: d[1], reverse=True) - l = int(rate * len(G)) - s = [] - for i in sorted_degree: - if len(s) < l: - s.append(i[0]) - for i in pos: - if i in SHS and i in s: - center[n][0] = pos[i][0] / 5 - center[n][1] = pos[i][1] / 5 - pos[i][0] = center[n][0] - pos[i][1] = center[n][1] - n += 1 - elif i in s: - node[m][0] = pos[i][0] - node[m][1] = pos[i][1] - m += 1 - node = node[0:m, :] - center = center[0:n, :] - if style == "side": - plt.scatter(node[:, 0], node[:, 1], marker=".", color="b", s=10) - plt.scatter(center[:, 0], center[:, 1], marker="*", color="r", s=20) - elif style == "center": - plt.scatter( - node[:, 0], - node[:, 1], - marker="o", - color="None", - edgecolors="b", - s=50, - linewidth=0.5, - ) - plt.scatter( - center[:, 0], - center[:, 1], - marker="o", - color="None", - edgecolors="r", - s=50, - linewidth=0.5, - ) - k = 0 - for i in pos: - if i in s: - if style == "side": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="top", - horizontalalignment="right", - ) - elif style == "center": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="center", - horizontalalignment="center", - ) - k += 1 - for i in G.edges: - (u, v, t) = i - if u in s and v in s: - p1 = [pos[i[0]][0], pos[i[1]][0]] - p2 = [pos[i[0]][1], pos[i[1]][1]] - plt.plot(p1, p2, "k-", alpha=0.3, linewidth=0.5) - plt.show() - return
- - -
[docs]def draw_kamada_kawai(G, rate=1, style="side"): - """Draw the graph G with a Kamada-Kawai force-directed layout. - - Parameters - ---------- - G : graph - A easygraph graph - - rate : float - The proportion of visible points and edges to the total - - style : string - "side"- the label is next to the dot - "center"- the label is in the center of the dot - - """ - import matplotlib.pyplot as plt - import numpy as np - - pos = eg.kamada_kawai_layout(G) - node = np.zeros((len(pos), 2), float) - m, n = 0, 0 - if rate == 1: - for i in pos: - node[m][0] = pos[i][0] - node[m][1] = pos[i][1] - m += 1 - if style == "side": - plt.scatter(node[:, 0], node[:, 1], marker=".", color="b", s=10) - elif style == "center": - plt.scatter( - node[:, 0], - node[:, 1], - marker="o", - color="None", - edgecolors="b", - s=50, - linewidth=0.5, - ) - k = 0 - for i in pos: - if style == "side": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="top", - horizontalalignment="right", - ) - elif style == "center": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="center", - horizontalalignment="center", - ) - k += 1 - for i in G.edges: - p1 = [pos[i[0]][0], pos[i[1]][0]] - p2 = [pos[i[0]][1], pos[i[1]][1]] - plt.plot(p1, p2, "k-", alpha=0.3, linewidth=0.5) - plt.show() - else: - degree = G.degree() - sorted_degree = sorted(degree.items(), key=lambda d: d[1], reverse=True) - l = int(rate * len(G)) - s = [] - for i in sorted_degree: - if len(s) < l: - s.append(i[0]) - for i in pos: - if i in s: - node[m][0] = pos[i][0] - node[m][1] = pos[i][1] - m += 1 - node = node[0:m, :] - if style == "side": - plt.scatter(node[:, 0], node[:, 1], marker=".", color="b", s=10) - elif style == "center": - plt.scatter( - node[:, 0], - node[:, 1], - marker="o", - color="None", - edgecolors="b", - s=50, - linewidth=0.5, - ) - k = 0 - for i in pos: - if i in s: - if style == "side": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="top", - horizontalalignment="right", - ) - elif style == "center": - plt.text( - pos[i][0], - pos[i][1], - i, - fontsize=5, - verticalalignment="center", - horizontalalignment="center", - ) - k += 1 - for i in G.edges: - (u, v, t) = i - if u in s and v in s: - p1 = [pos[i[0]][0], pos[i[1]][0]] - p2 = [pos[i[0]][1], pos[i[1]][1]] - plt.plot(p1, p2, "k-", alpha=0.3, linewidth=0.5) - plt.show() - return
- - -if __name__ == "__main__": - G = eg.datasets.get_graph_karateclub() - draw_SHS_center(G, [1, 33, 34], style="side") - draw_SHS_center(G, [1, 33, 34], style="center") - draw_SHS_center_kk(G, [1, 33, 34], style="side") - draw_SHS_center_kk(G, [1, 33, 34], style="center") - draw_kamada_kawai(G, style="side") - draw_kamada_kawai(G, style="center") - draw_SHS_center(G, [1, 33, 34], rate=0.8, style="side") - draw_SHS_center(G, [1, 33, 34], rate=0.8, style="center") - draw_SHS_center_kk(G, [1, 33, 34], rate=0.8, style="side") - draw_SHS_center_kk(G, [1, 33, 34], rate=0.8, style="center") - draw_kamada_kawai(G, rate=0.8, style="side") - draw_kamada_kawai(G, rate=0.8, style="center") -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/drawing/geometry.html b/docs/_modules/easygraph/functions/drawing/geometry.html deleted file mode 100644 index 5bdafa4e..00000000 --- a/docs/_modules/easygraph/functions/drawing/geometry.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - easygraph.functions.drawing.geometry — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.drawing.geometry

-import math
-
-from math import pi
-
-
-
[docs]def radian_from_atan(x, y): - if x == 0: - return pi / 2 if y > 0 else 3 * pi / 2 - if y == 0: - return 0 if x > 0 else pi - r = math.atan(y / x) - if x > 0 and y > 0: - return r - elif x > 0 and y < 0: - return r + 2 * pi - elif x < 0 and y > 0: - return r + pi - else: - return r + pi
- - -
[docs]def vlen(vector): - return math.sqrt(vector[0] ** 2 + vector[1] ** 2)
- - -
[docs]def common_tangent_radian(r1, r2, d): - alpha = math.acos(abs(r2 - r1) / d) - alpha = alpha if r1 > r2 else pi - alpha - return alpha
- - -
[docs]def polar_position(r, theta, start_point): - import numpy as np - - x = r * math.cos(theta) - y = r * math.sin(theta) - return np.array([x, y]) + start_point
- - -
[docs]def rad_2_deg(rad): - return rad * 180 / pi
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/drawing/layout.html b/docs/_modules/easygraph/functions/drawing/layout.html deleted file mode 100644 index 4c5b376b..00000000 --- a/docs/_modules/easygraph/functions/drawing/layout.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - easygraph.functions.drawing.layout — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.drawing.layout

-from typing import List
-
-from .simulator import Simulator
-from .utils import edge_list_to_incidence_matrix
-from .utils import init_pos
-
-
-
[docs]def force_layout( - num_v: int, - e_list: List[tuple], - push_v_strength: float, - push_e_strength: float, - pull_e_strength: float, - pull_center_strength: float, -): - import numpy as np - - v_coor = init_pos(num_v, scale=5) - assert v_coor.max() <= 5.0 and v_coor.min() >= -5.0 - centers = [np.array([0, 0])] - sim = Simulator( - nums=num_v, - forces={ - Simulator.NODE_ATTRACTION: pull_e_strength, - Simulator.NODE_REPULSION: push_v_strength, - Simulator.EDGE_REPULSION: push_e_strength, - Simulator.CENTER_GRAVITY: pull_center_strength, - }, - centers=centers, - ) - v_coor = sim.simulate(v_coor, edge_list_to_incidence_matrix(num_v, e_list)) - v_coor = (v_coor - v_coor.min(0)) / (v_coor.max(0) - v_coor.min(0)) * 0.8 + 0.1 - return v_coor
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/drawing/plot.html b/docs/_modules/easygraph/functions/drawing/plot.html deleted file mode 100644 index c94d3ba7..00000000 --- a/docs/_modules/easygraph/functions/drawing/plot.html +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - easygraph.functions.drawing.plot — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.drawing.plot

-import easygraph as eg
-
-
-__all__ = [
-    "plot_Followers",
-    "plot_Connected_Communities",
-    "plot_Betweenness_Centrality",
-    "plot_Neighborhood_Followers",
-]
-
-
-# Number of Followers
-
[docs]def plot_Followers(G, SHS): - """ - Returns the CDF curves of "Number of Followers" of SH spanners and ordinary users in graph G. - - Parameters - ---------- - G : graph - A easygraph graph. - - SHS : list - The SH Spanners in graph G. - - Returns - ------- - plt : CDF curves - the CDF curves of "Number of Followers" of SH spanners and ordinary users in graph G. - """ - import matplotlib.pyplot as plt - import numpy as np - import statsmodels.api as sm - - OU = [] - for i in G: - if i not in SHS: - OU.append(i) - degree = G.degree() - sample1 = [] - sample2 = [] - for i in degree.keys(): - if i in OU: - sample1.append(degree[i]) - elif i in SHS: - sample2.append(degree[i]) - X1 = np.linspace(min(sample1), max(sample1)) - ecdf = sm.distributions.ECDF(sample1) - Y1 = ecdf(X1) - X2 = np.linspace(min(sample2), max(sample2)) - ecdf = sm.distributions.ECDF(sample2) - Y2 = ecdf(X2) - plt.plot(X1, Y1, "b--", label="Ordinary User") - plt.plot(X2, Y2, "r", label="SH Spanner") - plt.title("Number of Followers") - plt.xlabel("Number of Followers") - plt.ylabel("Cumulative Distribution Function") - plt.legend(loc="lower right") - plt.show()
- - -# Number of Connected Communities -
[docs]def plot_Connected_Communities(G, SHS): - """ - Returns the CDF curves of "Number of Connected Communities" of SH spanners and ordinary users in graph G. - - Parameters - ---------- - G : graph - A easygraph graph. - - SHS : list - The SH Spanners in graph G. - - Returns - ------- - plt : CDF curves - the CDF curves of "Number of Connected Communities" of SH spanners and ordinary users in graph G. - """ - import matplotlib.pyplot as plt - import numpy as np - import statsmodels.api as sm - - OU = [] - for i in G: - if i not in SHS: - OU.append(i) - sample1 = [] - sample2 = [] - cmts = eg.LPA(G) - for i in OU: - s = set() - neighbors = G.neighbors(node=i) - for j in neighbors: - for k in cmts: - if j in cmts[k]: - s.add(k) - sample1.append(len(s)) - for i in SHS: - s = set() - neighbors = G.neighbors(node=i) - for j in neighbors: - for k in cmts: - if j in cmts[k]: - s.add(k) - sample2.append(len(s)) - print(len(cmts)) - print(sample1) - print(sample2) - X1 = np.linspace(min(sample1), max(sample1)) - ecdf = sm.distributions.ECDF(sample1) - Y1 = ecdf(X1) - X2 = np.linspace(min(sample2), max(sample2)) - ecdf = sm.distributions.ECDF(sample2) - Y2 = ecdf(X2) - plt.plot(X1, Y1, "b--", label="Ordinary User") - plt.plot(X2, Y2, "r", label="SH Spanner") - plt.title("Number of Connected Communities") - plt.xlabel("Number of Connected Communities") - plt.ylabel("Cumulative Distribution Function") - plt.legend(loc="lower right") - plt.show()
- - -# Betweenness Centrality -
[docs]def plot_Betweenness_Centrality(G, SHS): - """ - Returns the CDF curves of "Betweenness Centralitys" of SH spanners and ordinary users in graph G. - - Parameters - ---------- - G : graph - A easygraph graph. - - SHS : list - The SH Spanners in graph G. - - Returns - ------- - plt : CDF curves - the CDF curves of "Betweenness Centrality" of SH spanners and ordinary users in graph G. - """ - import matplotlib.pyplot as plt - import numpy as np - import statsmodels.api as sm - - OU = [] - for i in G: - if i not in SHS: - OU.append(i) - bc = eg.betweenness_centrality(G) - sample1 = [] - sample2 = [] - for i in bc.keys(): - if i in OU: - sample1.append(bc[i]) - elif i in SHS: - sample2.append(bc[i]) - X1 = np.linspace(min(sample1), max(sample1)) - ecdf = sm.distributions.ECDF(sample1) - Y1 = ecdf(X1) - X2 = np.linspace(min(sample2), max(sample2)) - ecdf = sm.distributions.ECDF(sample2) - Y2 = ecdf(X2) - plt.plot(X1, Y1, "b--", label="Ordinary User") - plt.plot(X2, Y2, "r", label="SH Spanner") - plt.title("Betweenness Centrality") - plt.xlabel("Betweenness Centrality") - plt.ylabel("Cumulative Distribution Function") - plt.legend(loc="lower right") - plt.show()
- - -# Arg. Number of Followers of the Neighborhood Users -
[docs]def plot_Neighborhood_Followers(G, SHS): - """ - Returns the CDF curves of "Arg. Number of Followers of the Neighborhood Users" of SH spanners and ordinary users in graph G. - - Parameters - ---------- - G : graph - A easygraph graph. - - SHS : list - The SH Spanners in graph G. - - Returns - ------- - plt : CDF curves - the CDF curves of "Arg. Number of Followers of the Neighborhood Users - " of SH spanners and ordinary users in graph G. - """ - import matplotlib.pyplot as plt - import numpy as np - import statsmodels.api as sm - - OU = [] - for i in G: - if i not in SHS: - OU.append(i) - sample1 = [] - sample2 = [] - degree = G.degree() - for i in OU: - num = 0 - sum = 0 - for neighbor in G.neighbors(node=i): - num = num + 1 - sum = sum + degree[neighbor] - sample1.append(sum / num) - for i in SHS: - num = 0 - sum = 0 - for neighbor in G.neighbors(node=i): - num = num + 1 - sum = sum + degree[neighbor] - sample2.append(sum / num) - X1 = np.linspace(min(sample1), max(sample1)) - ecdf = sm.distributions.ECDF(sample1) - Y1 = ecdf(X1) - X2 = np.linspace(min(sample2), max(sample2)) - ecdf = sm.distributions.ECDF(sample2) - Y2 = ecdf(X2) - plt.plot(X1, Y1, "b--", label="Ordinary User") - plt.plot(X2, Y2, "r", label="SH Spanner") - plt.title("Arg. Number of Followers of the Neighborhood Users") - plt.xlabel("Arg. Number of Followers of the Neighborhood Users") - plt.ylabel("Cumulative Distribution Function") - plt.legend(loc="lower right") - plt.show()
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/drawing/positioning.html b/docs/_modules/easygraph/functions/drawing/positioning.html deleted file mode 100644 index ea5ee32a..00000000 --- a/docs/_modules/easygraph/functions/drawing/positioning.html +++ /dev/null @@ -1,440 +0,0 @@ - - - - - - easygraph.functions.drawing.positioning — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.drawing.positioning

-import easygraph as eg
-
-
-__all__ = [
-    "random_position",
-    "circular_position",
-    "shell_position",
-    "rescale_position",
-    "kamada_kawai_layout",
-]
-
-
-
[docs]def random_position(G, center=None, dim=2, random_seed=None): - """ - Returns random position for each node in graph G. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - center : array-like or None, optional (default : None) - Coordinate pair around which to center the layout - - dim : int, optional (default : 2) - Dimension of layout - - random_seed : int or None, optional (default : None) - Seed for RandomState instance - - Returns - ---------- - pos : dict - A dictionary of positions keyed by node - """ - import numpy as np - - center = _get_center(center, dim) - - rng = np.random.RandomState(seed=random_seed) - pos = rng.rand(len(G), dim) + center - pos = pos.astype(np.float32) - pos = dict(zip(G, pos)) - - return pos
- - -
[docs]def circular_position(G, center=None, scale=1): - """ - Position nodes on a circle, the dimension is 2. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - A position will be assigned to every node in G - - center : array-like or None, optional (default : None) - Coordinate pair around which to center the layout - - scale : number, optional (default : 1) - Scale factor for positions - - Returns - ------- - pos : dict - A dictionary of positions keyed by node - """ - import numpy as np - - center = _get_center(center, dim=2) - - if len(G) == 0: - pos = {} - elif len(G) == 1: - pos = {G.nodes[0]: center} - else: - theta = np.linspace(0, 1, len(G), endpoint=False) * 2 * np.pi - theta = theta.astype(np.float32) - pos = np.column_stack([np.cos(theta), np.sin(theta)]) - pos = rescale_position(pos, scale=scale) + center - pos = dict(zip(G, pos)) - - return pos
- - -
[docs]def shell_position(G, nlist=None, scale=1, center=None): - """ - Position nodes in concentric circles, the dimension is 2. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - nlist : list of lists or None, optional (default : None) - List of node lists for each shell. - - scale : number, optional (default : 1) - Scale factor for positions. - - center : array-like or None, optional (default : None) - Coordinate pair around which to center the layout. - - - Returns - ------- - pos : dict - A dictionary of positions keyed by node - - Notes - ----- - This algorithm currently only works in two dimensions and does not - try to minimize edge crossings. - - """ - import numpy as np - - center = _get_center(center, dim=2) - - if len(G) == 0: - return {} - if len(G) == 1: - return {G.nodes[0]: center} - - if nlist is None: - # draw the whole graph in one shell - nlist = [list(G)] - - if len(nlist[0]) == 1: - # single node at center - radius = 0.0 - else: - # else start at r=1 - radius = 1.0 - - npos = {} - for nodes in nlist: - # Discard the extra angle since it matches 0 radians. - theta = np.linspace(0, 1, len(nodes), endpoint=False) * 2 * np.pi - theta = theta.astype(np.float32) - pos = np.column_stack([np.cos(theta), np.sin(theta)]) - if len(pos) > 1: - pos = rescale_position(pos, scale=scale * radius / len(nlist)) + center - else: - pos = np.array([(scale * radius + center[0], center[1])]) - npos.update(zip(nodes, pos)) - radius += 1.0 - - return npos
- - -def _get_center(center, dim): - import numpy as np - - if center is None: - center = np.zeros(dim) - else: - center = np.asarray(center) - - if dim < 2: - raise ValueError("cannot handle dimensions < 2") - - if len(center) != dim: - msg = "length of center coordinates must match dimension of layout" - raise ValueError(msg) - - return center - - -
[docs]def rescale_position(pos, scale=1): - """ - Returns scaled position array to (-scale, scale) in all axes. - - Parameters - ---------- - pos : numpy array - positions to be scaled. Each row is a position. - - scale : number, optional (default : 1) - The size of the resulting extent in all directions. - - Returns - ------- - pos : numpy array - scaled positions. Each row is a position. - """ - # Find max length over all dimensions - lim = 0 # max coordinate for all axes - for i in range(pos.shape[1]): - pos[:, i] -= pos[:, i].mean() - lim = max(abs(pos[:, i]).max(), lim) - # rescale to (-scale, scale) in all directions, preserves aspect - if lim > 0: - for i in range(pos.shape[1]): - pos[:, i] *= scale / lim - return pos
- - -
[docs]def kamada_kawai_layout( - G, dist=None, pos=None, weight="weight", scale=1, center=None, dim=2 -): - """Position nodes using Kamada-Kawai basic-length cost-function. - - Parameters - ---------- - G : graph or list of nodes - A position will be assigned to every node in G. - - dist : dict (default=None) - A two-level dictionary of optimal distances between nodes, - indexed by source and destination node. - If None, the distance is computed using shortest_path_length(). - - pos : dict or None optional (default=None) - Initial positions for nodes as a dictionary with node as keys - and values as a coordinate list or tuple. If None, then use - circular_layout() for dim >= 2 and a linear layout for dim == 1. - - weight : string or None optional (default='weight') - The edge attribute that holds the numerical value used for - the edge weight. If None, then all edge weights are 1. - - scale : number (default: 1) - Scale factor for positions. - - center : array-like or None - Coordinate pair around which to center the layout. - - dim : int - Dimension of layout. - - Returns - ------- - pos : dict - A dictionary of positions keyed by node - - Examples - -------- - >>> pos = eg.kamada_kawai_layout(G) - """ - import numpy as np - - nNodes = len(G) - if nNodes == 0: - return {} - - if dist is None: - dist = dict(eg.Floyd(G)) - dist_mtx = 1e6 * np.ones((nNodes, nNodes)) - for row, nr in enumerate(G): - if nr not in dist: - continue - rdist = dist[nr] - for col, nc in enumerate(G): - if nc not in rdist: - continue - dist_mtx[row][col] = rdist[nc] - - if pos is None: - if dim >= 3: - pos = eg.random_position(G, dim=dim) - elif dim == 2: - pos = eg.circular_position(G) - else: - pos = {n: pt for n, pt in zip(G, np.linspace(0, 1, len(G)))} - - pos_arr = np.array([pos[n] for n in G]) - - pos = _kamada_kawai_solve(dist_mtx, pos_arr, dim) - - if center is None: - center = np.zeros(dim) - else: - center = np.asarray(center) - - if len(center) != dim: - msg = "length of center coordinates must match dimension of layout" - raise ValueError(msg) - - pos = eg.rescale_position(pos, scale=scale) + center - return dict(zip(G, pos))
- - -def _kamada_kawai_solve(dist_mtx, pos_arr, dim): - # Anneal node locations based on the Kamada-Kawai cost-function, - # using the supplied matrix of preferred inter-node distances, - # and starting locations. - - import numpy as np - - from scipy.optimize import minimize - - meanwt = 1e-3 - costargs = (np, 1 / (dist_mtx + np.eye(dist_mtx.shape[0]) * 1e-3), meanwt, dim) - - optresult = minimize( - _kamada_kawai_costfn, - pos_arr.ravel(), - method="L-BFGS-B", - args=costargs, - jac=True, - ) - - return optresult.x.reshape((-1, dim)) - - -def _kamada_kawai_costfn(pos_vec, np, invdist, meanweight, dim): - # Cost-function and gradient for Kamada-Kawai layout algorithm - nNodes = invdist.shape[0] - pos_arr = pos_vec.reshape((nNodes, dim)) - - delta = pos_arr[:, np.newaxis, :] - pos_arr[np.newaxis, :, :] - nodesep = np.linalg.norm(delta, axis=-1) - direction = np.einsum("ijk,ij->ijk", delta, 1 / (nodesep + np.eye(nNodes) * 1e-3)) - - offset = nodesep * invdist - 1.0 - offset[np.diag_indices(nNodes)] = 0 - - cost = 0.5 * np.sum(offset**2) - grad = np.einsum("ij,ij,ijk->ik", invdist, offset, direction) - np.einsum( - "ij,ij,ijk->jk", invdist, offset, direction - ) - - # Additional parabolic term to encourage mean position to be near origin: - sumpos = np.sum(pos_arr, axis=0) - cost += 0.5 * meanweight * np.sum(sumpos**2) - grad += meanweight * sumpos - - return (cost, grad.ravel()) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/drawing/simulator.html b/docs/_modules/easygraph/functions/drawing/simulator.html deleted file mode 100644 index 2603055f..00000000 --- a/docs/_modules/easygraph/functions/drawing/simulator.html +++ /dev/null @@ -1,310 +0,0 @@ - - - - - - easygraph.functions.drawing.simulator — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.drawing.simulator

-from copy import deepcopy
-
-from .utils import safe_div
-
-
-
[docs]class Simulator: - NODE_ATTRACTION = 0 - NODE_REPULSION = 1 - EDGE_REPULSION = 2 - CENTER_GRAVITY = 3 - - def __init__(self, nums, forces, centers=1, damping_factor=0.999) -> None: - self.nums = [nums] if isinstance(nums, int) else nums - - self.node_attraction = forces.get(self.NODE_ATTRACTION, None) - self.node_repulsion = forces.get(self.NODE_REPULSION, None) - self.edge_repulsion = forces.get(self.EDGE_REPULSION, None) - self.center_gravity = forces.get(self.CENTER_GRAVITY, None) - - self.n_centers = len(centers) - self.centers = centers - - if self.node_repulsion is not None and isinstance(self.node_repulsion, float): - self.node_repulsion = [self.node_repulsion] * self.n_centers - if self.center_gravity is not None and isinstance(self.center_gravity, float): - self.center_gravity = [self.center_gravity] * self.n_centers - - self.damping_factor = damping_factor - -
[docs] def simulate(self, init_position, H, max_iter=400, epsilon=0.001, dt=2.0) -> None: - import numpy as np - - """ - Simulate the force-directed layout algorithm. - """ - position = init_position.copy() - velocity = np.zeros_like(position) - damping = 1.0 - for it in range(max_iter): - position, velocity, stop = self._step( - position, velocity, H, epsilon, damping, dt - ) - # np.save("./tmp/position_{}.npy".format(it), position) - # np.save("./tmp/velocity_{}.npy".format(it), velocity) - if stop: - break - damping *= self.damping_factor - return position
- - def _step(self, position, velocity, H, epsilon, damping, dt): - import numpy as np - - from sklearn.metrics import euclidean_distances - - """ - One step of the simulation. - """ - v2v_dist = euclidean_distances(position) - e_center = np.matmul(H.T, position) / H.sum(axis=0).reshape(-1, 1) - v2e_dist = euclidean_distances(position, e_center) * H - e2e_dist = euclidean_distances(e_center) - - centers = self.centers - - force = np.zeros_like(position) - if self.node_attraction is not None: - f = ( - self._node_attraction(position, e_center, v2e_dist) - * self.node_attraction - ) - assert np.isnan(f).sum() == 0 - force += f - if self.node_repulsion is not None: - f = self._node_repulsion(position, v2v_dist) - if self.n_centers == 1: - f *= self.node_repulsion[0] - else: - masks = np.zeros((position.shape[0], 1)) - masks[: self.nums[0]] = self.node_repulsion[0] - masks[self.nums[0] :] = self.node_repulsion[1] - f *= masks - assert np.isnan(f).sum() == 0 - force += f - if self.edge_repulsion is not None: - f = self._edge_repulsion(e_center, H, e2e_dist) * self.edge_repulsion - assert np.isnan(f).sum() == 0 - force += f - if self.center_gravity is not None: - masks = [np.zeros((position.shape[0], 1)), np.zeros((position.shape[0], 1))] - masks[0][: self.nums[0]] = 1 - masks[1][self.nums[0] :] = 1 - for center, gravity, mask in zip(centers, self.center_gravity, masks): - v2c_dist = euclidean_distances(position, center.reshape(1, -1)).reshape( - -1, 1 - ) - f = self._center_gravity(position, center, v2c_dist) * gravity * mask - assert np.isnan(f).sum() == 0 - force += f - - force *= damping - - force = np.clip(force, -0.1, 0.1) - position += force * dt - velocity = force - - return position, velocity, self._stop_condition(velocity, epsilon) - - def _node_attraction(self, position, e_center, v2e_dist, x0=0.1, k=1.0): - import numpy as np - - """ - Node attracted by edge center. - """ - x = deepcopy(v2e_dist) - x[v2e_dist > 0] -= x0 - f_scale = k * x # (n, m) - f_dir = ( - e_center[np.newaxis, :, :] - position[:, np.newaxis, :] - ) # (1, m, 2) - (n, 1, 2) -> (n, m, 2) - f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, m) - # f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, m, 2) - f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, m, 2) - f = f_scale[:, :, np.newaxis] * f_dir # (n, m, 2) - f = f.sum(axis=1) # (n, 2) - return f - - def _node_repulsion(self, position, v2v_dist, k=1.0): - import numpy as np - - """ - Node repulsed by other nodes. - """ - dist = v2v_dist.copy() - r, c = np.diag_indices_from(dist) - dist[r, c] = np.inf - - f_scale = k / (dist**2) # (n, n) with diag 0 - f_dir = ( - position[:, np.newaxis, :] - position[np.newaxis, :, :] - ) # (n, 1, 2) - (1, n, 2) -> (n, n, 2) - f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, n) - f_dir_len[r, c] = np.inf - # f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, n, 2) - f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, n, 2) - f = f_scale[:, :, np.newaxis] * f_dir # (n, n, 2) - f[r, c] = 0 - f = f.sum(axis=1) # (n, 2) - return f - - def _edge_repulsion(self, e_center, H, e2e_dist, k=1.0): - import numpy as np - - """ - Edge repulsed by other edges. - """ - dist = e2e_dist.copy() - r, c = np.diag_indices_from(dist) - dist[r, c] = np.inf - - f_scale = k / (dist**2) # (m, m) - f_dir = ( - e_center[:, np.newaxis, :] - e_center[np.newaxis, :, :] - ) # (m, 1, 2) - (1, m, 2) -> (m, m, 2) - f_dir_len = np.linalg.norm(f_dir, axis=2) # (m, m) - f_dir_len[r, c] = np.inf - # f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (m, m, 2) - f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (m, m, 2) - f = f_scale[:, :, np.newaxis] * f_dir # (m, m, 2) - f[r, c] = 0 - f = f.sum(axis=1) # (m, 2) - return np.matmul(H, f) - - def _center_gravity(self, position, center, v2c_dist, k=1): - import numpy as np - - """ - Node attracted by center. - """ - f_scale = v2c_dist # (n, 1) - f_dir = ( - center[np.newaxis, np.newaxis, :] - position[:, np.newaxis, :] - ) # (1, 1, 2) - (n, 1, 2) -> (n, 1, 2) - f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, 1) - # f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, 1, 2) - f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, 1, 2) - f = f_scale[:, :, np.newaxis] * f_dir # (n, 1, 2) - # f = jitter(f) - f = f.sum(axis=1) * k - return f - - def _stop_condition(self, velocity, epsilon): - import numpy as np - - """ - Stop condition. - """ - return np.linalg.norm(velocity) < epsilon
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/drawing/utils.html b/docs/_modules/easygraph/functions/drawing/utils.html deleted file mode 100644 index 77b78936..00000000 --- a/docs/_modules/easygraph/functions/drawing/utils.html +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - easygraph.functions.drawing.utils — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.drawing.utils

-from itertools import chain
-from typing import List
-from typing import Optional
-from typing import Tuple
-
-import matplotlib
-import numpy as np
-
-from matplotlib.axes import Axes
-from matplotlib.collections import PatchCollection
-from matplotlib.patches import Circle
-from matplotlib.patches import PathPatch
-from matplotlib.path import Path
-from scipy.spatial import ConvexHull
-
-from .geometry import common_tangent_radian
-from .geometry import polar_position
-from .geometry import rad_2_deg
-from .geometry import radian_from_atan
-from .geometry import vlen
-
-
-
[docs]def safe_div(a: np.ndarray, b: np.ndarray, jitter_scale: float = 0.000001): - mask = b == 0 - b[mask] = 1 - inv_b = 1.0 / b - res = a * inv_b - if mask.sum() > 0: - res[mask.repeat(2, 2)] = np.random.randn(mask.sum() * 2) * jitter_scale - return res
- - -
[docs]def init_pos(num_v: int, center: Tuple[float, float] = (0, 0), scale: float = 1.0): - return (np.random.rand(num_v, 2) * 2 - 1) * scale + center
- - -
[docs]def draw_line_edge( - ax: Axes, - v_coor: np.array, - v_size: list, - e_list: List[Tuple[int, int]], - show_arrow: bool, - e_color: list, - e_line_width: list, -): - arrow_head_width = ( - [0.015 * w for w in e_line_width] if show_arrow else [0] * len(e_list) - ) - - for eidx, e in enumerate(e_list): - start_pos = v_coor[e[0]] - end_pos = v_coor[e[1]] - - dir = end_pos - start_pos - dir = dir / np.linalg.norm(dir) - - start_pos = start_pos + dir * v_size[e[0]] - end_pos = end_pos - dir * v_size[e[1]] - - x, y = start_pos[0], start_pos[1] - dx, dy = end_pos[0] - x, end_pos[1] - y - - ax.arrow( - x, - y, - dx, - dy, - head_width=arrow_head_width[eidx], - color=e_color[eidx], - linewidth=e_line_width[eidx], - length_includes_head=True, - )
- - -
[docs]def draw_circle_edge( - ax: Axes, - v_coor: List[Tuple[float, float]], - v_size: list, - e_list: List[Tuple[int, int]], - e_color: list, - e_fill_color: list, - e_line_width: list, -): - n_v = len(v_coor) - line_paths, arc_paths, vertices = hull_layout(n_v, e_list, v_coor, v_size) - - for eidx, lines in enumerate(line_paths): - pathdata = [] - for line in lines: - if len(line) == 0: - continue - start_pos, end_pos = line - pathdata.append((Path.MOVETO, start_pos.tolist())) - pathdata.append((Path.LINETO, end_pos.tolist())) - - if len(list(zip(*pathdata))) == 0: - continue - codes, verts = zip(*pathdata) - path = Path(verts, codes) - ax.add_patch( - PathPatch( - path, - linewidth=e_line_width[eidx], - facecolor=e_fill_color[eidx], - edgecolor=e_color[eidx], - ) - ) - - for eidx, arcs in enumerate(arc_paths): - for arc in arcs: - center, theta1, theta2, radius = arc - x, y = center[0], center[1] - - ax.add_patch( - matplotlib.patches.Arc( - (x, y), - 2 * radius, - 2 * radius, - theta1=theta1, - theta2=theta2, - # color=e_color[eidx], - linewidth=e_line_width[eidx], - edgecolor=e_color[eidx], - facecolor=e_fill_color[eidx], - ) - )
- - -
[docs]def edge_list_to_incidence_matrix(num_v: int, e_list: List[tuple]) -> np.ndarray: - v_idx = list(chain(*e_list)) - e_idx = [[idx] * len(e) for idx, e in enumerate(e_list)] - e_idx = list(chain(*e_idx)) - H = np.zeros((num_v, len(e_list))) - H[v_idx, e_idx] = 1 - return H
- - -
[docs]def draw_vertex( - ax: Axes, - v_coor: List[Tuple[float, float]], - v_label: Optional[List[str]], - font_size: int, - font_family: str, - v_size: list, - v_color: list, - v_line_width: list, -): - patches = [] - n = v_coor.shape[0] - if v_label is None: - v_label = [""] * n - for coor, label, size, width in zip(v_coor.tolist(), v_label, v_size, v_line_width): - circle = Circle(coor, size) - circle.lineWidth = width - # circle.label = label - if label != "": - x, y = coor[0], coor[1] - offset = 0, -1.3 * size - x += offset[0] - y += offset[1] - ax.text( - x, - y, - label, - fontsize=font_size, - fontfamily=font_family, - ha="center", - va="top", - ) - patches.append(circle) - p = PatchCollection(patches, facecolors=v_color, edgecolors="black") - ax.add_collection(p)
- - -
[docs]def hull_layout(n_v, e_list, pos, v_size, radius_increment=0.3): - line_paths = [None] * len(e_list) - arc_paths = [None] * len(e_list) - - polygons_vertices_index = [] - vertices_radius = np.array(v_size) - vertices_increased_radius = vertices_radius * radius_increment - vertices_radius += vertices_increased_radius - - e_degree = [len(e) for e in e_list] - e_idxs = np.argsort(np.array(e_degree)) - - # for edge in e_list: - for e_idx in e_idxs: - edge = list(e_list[e_idx]) - - line_path_for_e = [] - arc_path_for_e = [] - - if len(edge) == 1: - arc_path_for_e.append([pos[edge[0]], 0, 360, vertices_radius[edge[0]]]) - - vertices_radius[edge] += vertices_increased_radius[edge] - - line_paths[e_idx] = line_path_for_e - arc_paths[e_idx] = arc_path_for_e - continue - - pos_in_edge = pos[edge] - if len(edge) == 2: - vertices_index = np.array((0, 1), dtype=np.int64) - else: - hull = ConvexHull(pos_in_edge) - vertices_index = hull.vertices - - n_vertices = vertices_index.shape[0] - - vertices_index = np.append(vertices_index, vertices_index[0]) # close the loop - - thetas = [] - - for i in range(n_vertices): - # line - i1 = edge[vertices_index[i]] - i2 = edge[vertices_index[i + 1]] - - r1 = vertices_radius[i1] - r2 = vertices_radius[i2] - - p1 = pos[i1] - p2 = pos[i2] - - dp = p2 - p1 - dp_len = vlen(dp) - - beta = radian_from_atan(dp[0], dp[1]) - alpha = common_tangent_radian(r1, r2, dp_len) - - theta = beta - alpha - start_point = polar_position(r1, theta, p1) - end_point = polar_position(r2, theta, p2) - - line_path_for_e.append((start_point, end_point)) - thetas.append(theta) - - for i in range(n_vertices): - # arcs - theta_1 = thetas[i - 1] - theta_2 = thetas[i] - - arc_center = pos[edge[vertices_index[i]]] - radius = vertices_radius[edge[vertices_index[i]]] - - theta_1, theta_2 = rad_2_deg(theta_1), rad_2_deg(theta_2) - arc_path_for_e.append((arc_center, theta_1, theta_2, radius)) - - vertices_radius[edge] += vertices_increased_radius[edge] - - polygons_vertices_index.append(vertices_index.copy()) - - # line_paths.append(line_path_for_e) - # arc_paths.append(arc_path_for_e) - line_paths[e_idx] = line_path_for_e - arc_paths[e_idx] = arc_path_for_e - - return line_paths, arc_paths, polygons_vertices_index
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/graph_embedding/NOBE.html b/docs/_modules/easygraph/functions/graph_embedding/NOBE.html deleted file mode 100644 index e3813b16..00000000 --- a/docs/_modules/easygraph/functions/graph_embedding/NOBE.html +++ /dev/null @@ -1,297 +0,0 @@ - - - - - - easygraph.functions.graph_embedding.NOBE — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.graph_embedding.NOBE

-import easygraph as eg
-import numpy as np
-
-from easygraph.utils import *
-
-
-__all__ = ["NOBE", "NOBE_GA"]
-
-
-
[docs]@not_implemented_for("multigraph") -def NOBE(G, K): - """Graph embedding via NOBE[1]. - - Parameters - ---------- - G : easygraph.Graph - An unweighted and undirected graph. - - K : int - Embedding dimension k - - Returns - ------- - Y : list - list of embedding vectors (y1, y2, · · · , yn) - - Examples - -------- - >>> NOBE(G,K=15) - - References - ---------- - .. [1] https://www.researchgate.net/publication/325004496_On_Spectral_Graph_Embedding_A_Non-Backtracking_Perspective_and_Graph_Approximation - - """ - dict = {} - a = 0 - for i in G.nodes: - dict[i] = a - a += 1 - LG = graph_to_d_atleast2(G) - N = len(G) - P, pair = Transition(LG) - V = eigs_nodes(P, K) - Y = embedding(V, pair, K, N, dict, G) - return Y
- - -
[docs]@not_implemented_for("multigraph") -def NOBE_GA(G, K): - """Graph embedding via NOBE-GA[1]. - - Parameters - ---------- - G : easygraph.Graph - An unweighted and undirected graph. - - K : int - Embedding dimension k - - Returns - ------- - Y : list - list of embedding vectors (y1, y2, · · · , yn) - - Examples - -------- - >>> NOBE_GA(G,K=15) - - References - ---------- - .. [1] https://www.researchgate.net/publication/325004496_On_Spectral_Graph_Embedding_A_Non-Backtracking_Perspective_and_Graph_Approximation - - """ - from scipy.sparse.linalg import eigs - - N = len(G) - A = np.eye(N, N) - for i in G.edges: - (u, v, t) = i - u = int(u) - 1 - v = int(v) - 1 - A[u, v] = 1 - degree = G.degree() - D_inv = np.zeros([N, N]) - a = 0 - for i in degree: - D_inv[a, a] = 1 / degree[i] - a += 1 - D_I_inv = np.zeros([N, N]) - b = 0 - for i in degree: - if degree[i] > 1: - D_I_inv[b, b] = 1 / (degree[i] - 1) - b += 1 - I = np.identity(N) - M_D = 0.5 * A * D_I_inv * (I - D_inv) - D_D = 0.5 * I - T_ua = np.zeros([2 * N, 2 * N]) - T_ua[0:N, 0:N] = M_D - T_ua[N : 2 * N, N : 2 * N] = M_D - T_ua[N : 2 * N, 0:N] = D_D - T_ua[0:N, N : 2 * N] = D_D - Y1, Y = eigs(T_ua, K + 1, which="LR") - Y = Y[0:N, :-1] - return Y
- - -def graph_to_d_atleast2(G): - n = len(G) - LG = eg.Graph() - LG = G.copy() - new_node = n - degree = LG.degree() - node = LG.nodes.copy() - for i in node: - if degree[i] == 1: - for neighbors in LG.neighbors(node=i): - LG.add_edge(i, new_node) - LG.add_edge(new_node, neighbors) - break - new_node = new_node + 1 - return LG - - -def Transition(LG): - N = len(LG) - M = LG.size() - LLG = eg.DiGraph() - for i in LG.edges: - (u, v, t) = i - LLG.add_edge(u, v) - LLG.add_edge(v, u) - degree = LLG.degree() - P = np.zeros([2 * M, 2 * M]) - pair = [] - k = 0 - l = 0 - for i in LLG.edges: - l = 0 - for j in LLG.edges: - (u, v, t) = i - (x, y, z) = j - if v == x and u != y: - P[k][l] = 1 / (degree[v] - 1) - l += 1 - k += 1 - a = 0 - for i in LLG.edges: - (u, v, t) = i - pair.append([u, v]) - a += 1 - return P, pair - - -def eigs_nodes(P, K): - from scipy.sparse.linalg import eigs - - M = np.size(P, 0) - L = np.zeros([M, M]) - I = np.identity(M) - P_T = P.T - L = I - (P + P_T) / 2 - U, D = eigs(L, K + 1, which="LR") - D = D[:, :-1] - V = np.zeros([M, K], dtype=complex) - a = 0 - for i in D: - V[a] = i - a += 1 - return V - - -def embedding(V, pair, K, N, dict, G): - Y = np.zeros([N, K], dtype=complex) - idx = 0 - for i in pair: - [v, u] = i - if u in G.nodes: - t = dict[u] - for j in range(0, len(V[idx])): - Y[t, j] += V[idx, j] - idx += 1 - return Y -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/graph_embedding/deepwalk.html b/docs/_modules/easygraph/functions/graph_embedding/deepwalk.html deleted file mode 100644 index 831b3f3d..00000000 --- a/docs/_modules/easygraph/functions/graph_embedding/deepwalk.html +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - easygraph.functions.graph_embedding.deepwalk — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.graph_embedding.deepwalk

-import random
-
-from easygraph.functions.graph_embedding.node2vec import (
-    _get_embedding_result_from_gensim_skipgram_model,
-)
-from easygraph.functions.graph_embedding.node2vec import learn_embeddings
-from easygraph.utils import *
-from tqdm import tqdm
-
-
-__all__ = ["deepwalk"]
-
-
-
[docs]@not_implemented_for("multigraph") -def deepwalk(G, dimensions=128, walk_length=80, num_walks=10, **skip_gram_params): - """Graph embedding via DeepWalk. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - dimensions : int - Embedding dimensions, optional(default: 128) - - walk_length : int - Number of nodes in each walk, optional(default: 80) - - num_walks : int - Number of walks per node, optional(default: 10) - - skip_gram_params : dict - Parameters for gensim.models.Word2Vec - do not supply `size`, it is taken from the `dimensions` parameter - - Returns - ------- - embedding_vector : dict - The embedding vector of each node - - most_similar_nodes_of_node : dict - The most similar nodes of each node and its similarity - - Examples - -------- - - >>> deepwalk(G, - ... dimensions=128, # The graph embedding dimensions. - ... walk_length=80, # Walk length of each random walks. - ... num_walks=10, # Number of random walks. - ... skip_gram_params = dict( # The skip_gram parameters in Python package gensim. - ... window=10, - ... min_count=1, - ... batch_words=4, - ... iter=15 - ... )) - - References - ---------- - .. [1] https://arxiv.org/abs/1403.6652 - - """ - G_index, index_of_node, node_of_index = G.to_index_node_graph() - - walks = simulate_walks(G_index, walk_length=walk_length, num_walks=num_walks) - model = learn_embeddings(walks=walks, dimensions=dimensions, **skip_gram_params) - - ( - embedding_vector, - most_similar_nodes_of_node, - ) = _get_embedding_result_from_gensim_skipgram_model( - G=G, index_of_node=index_of_node, node_of_index=node_of_index, model=model - ) - - del G_index - return embedding_vector, most_similar_nodes_of_node
- - -def simulate_walks(G, walk_length, num_walks): - walks = [] - nodes = list(G.nodes) - print("Walk iteration:") - for walk_iter in tqdm(range(num_walks)): - random.shuffle(nodes) - for node in nodes: - walks.append(_deepwalk_walk(G, walk_length=walk_length, start_node=node)) - - return walks - - -def _deepwalk_walk(G, walk_length, start_node): - """ - Simulate a random walk starting from start node. - """ - walk = [start_node] - - while len(walk) < walk_length: - cur = walk[-1] - cur_nbrs = sorted(G.neighbors(cur)) - if len(cur_nbrs) > 0: - pick_node = random.choice(cur_nbrs) - walk.append(pick_node) - else: - break - return walk -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/graph_embedding/line.html b/docs/_modules/easygraph/functions/graph_embedding/line.html deleted file mode 100644 index 24a8d4f9..00000000 --- a/docs/_modules/easygraph/functions/graph_embedding/line.html +++ /dev/null @@ -1,416 +0,0 @@ - - - - - - easygraph.functions.graph_embedding.line — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.graph_embedding.line

-import time
-import warnings
-
-import easygraph as eg
-import numpy as np
-import torch
-import torch.nn as nn
-
-from easygraph.utils import alias_draw
-from easygraph.utils import alias_setup
-from sklearn import preprocessing
-
-# from easygraph.functions.graph_embedding import *
-from tqdm import tqdm
-
-
-warnings.filterwarnings("ignore")
-
-
-
[docs]class LINE(nn.Module): - """Graph embedding via LINE. - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - dimension: int - walk_length: int - - walk_num: int - - negative: int - batch_size: int - - init_alpha: float - order: int - Returns - ------- - embedding_vector : dict - The embedding vector of each node - Examples - -------- - >>> model = LINE( - ... dimension=128, - ... walk_length=80, - ... walk_num=20, - ... negative=5, - ... batch_size=128, - ... init_alpha=0.025, - ... order=3 ) - >>> model.train() - >>> emb = model(g, return_dict=True) # g: easygraph.Graph or easygraph.DiGraph - - References - ---------- - - .. [1] Tang, J., Qu, M., Wang, M., Zhang, M., Yan, J., & Mei, Q. (2015, May). Line: Large-scale information network embedding. In Proceedings of the 24th international conference on world wide web (pp. 1067-1077). - - https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/frp0228-Tang.pdf - - """ - -
[docs] @staticmethod - def add_args(parser): - """Add model-specific arguments to the parser.""" - parser.add_argument( - "--walk-length", - type=int, - default=80, - help="Length of walk per source. Default is 80.", - ) - parser.add_argument( - "--walk-num", - type=int, - default=20, - help="Number of walks per source. Default is 20.", - ) - parser.add_argument( - "--negative", - type=int, - default=5, - help="Number of negative node in sampling. Default is 5.", - ) - parser.add_argument( - "--batch-size", - type=int, - default=1000, - help="Batch size in SGD training process. Default is 1000.", - ) - parser.add_argument( - "--alpha", - type=float, - default=0.025, - help="Initial learning rate of SGD. Default is 0.025.", - ) - parser.add_argument( - "--order", - type=int, - default=3, - help="Order of proximity in LINE. Default is 3 for 1+2.", - ) - parser.add_argument("--hidden-size", type=int, default=128)
- -
[docs] @classmethod - def build_model_from_args(cls, args): - return cls( - args.hidden_size, - args.walk_length, - args.walk_num, - args.negative, - args.batch_size, - args.alpha, - args.order, - )
- - def __init__( - self, - dimension=128, - walk_length=80, - walk_num=20, - negative=5, - batch_size=128, - init_alpha=0.025, - order=3, - ): - super(LINE, self).__init__() - self.dimension = dimension - self.walk_length = walk_length - self.walk_num = walk_num - self.negative = negative - self.batch_size = batch_size - self.init_alpha = init_alpha - self.order = order - -
[docs] def forward(self, g, return_dict=True): - # run LINE algorithm, 1-order, 2-order or 3(1-order + 2-order) - - self.G = g - self.is_directed = g.is_directed() - self.num_node = g.size() - self.num_edge = g.number_of_edges() - self.num_sampling_edge = self.walk_length * self.walk_num * self.num_node - - node2id = dict([(node, vid) for vid, node in enumerate(g.nodes)]) - self.edges = [[node2id[e[0]], node2id[e[1]]] for e in self.G.edges] - - self.edges_prob = np.asarray([1.0 for e in g.edges]) - self.edges_prob /= np.sum(self.edges_prob) - self.edges_table, self.edges_prob = alias_setup(self.edges_prob) - - degree_weight = np.asarray([0] * self.num_node) - degree_weight = np.array(list(g.degree(node2id[u] for u in g.nodes).values())) - # for u,v in g.edges: - - # degree_weight[node2id[u]] += 1.0 - # if not self.is_directed: - # degree_weight[node2id[v]] += 1.0 - self.node_prob = np.power(degree_weight, 0.75) - self.node_prob /= np.sum(self.node_prob) - self.node_table, self.node_prob = alias_setup(self.node_prob) - - if self.order == 3: - self.dimension = int(self.dimension / 2) - if self.order == 1 or self.order == 3: - print("train line with 1-order") - print(type(self.dimension)) - self.emb_vertex = ( - np.random.random((self.num_node, self.dimension)) - 0.5 - ) / self.dimension - self._train_line(order=1) - embedding1 = preprocessing.normalize(self.emb_vertex, "l2") - - if self.order == 2 or self.order == 3: - print("train line with 2-order") - self.emb_vertex = ( - np.random.random((self.num_node, self.dimension)) - 0.5 - ) / self.dimension - self.emb_context = self.emb_vertex - self._train_line(order=2) - embedding2 = preprocessing.normalize(self.emb_vertex, "l2") - - if self.order == 1: - embeddings = embedding1 - elif self.order == 2: - embeddings = embedding2 - else: - print("concatenate two embedding...") - embeddings = np.hstack((embedding1, embedding2)) - - if return_dict: - features_matrix = dict() - for vid, node in enumerate(g.nodes): - features_matrix[node] = embeddings[vid] - else: - features_matrix = np.zeros((g.num_nodes, embeddings.shape[1])) - nx_nodes = g.nodes() - features_matrix[nx_nodes] = embeddings[np.arange(g.num_nodes)] - return features_matrix
- - def _update(self, vec_u, vec_v, vec_error, label): - # update vetex embedding and vec_error - f = 1 / (1 + np.exp(-np.sum(vec_u * vec_v, axis=1))) - g = (self.alpha * (label - f)).reshape((len(label), 1)) - vec_error += g * vec_v - vec_v += g * vec_u - - def _train_line(self, order): - # train Line model with order - self.alpha = self.init_alpha - batch_size = self.batch_size - t0 = time.time() - num_batch = int(self.num_sampling_edge / batch_size) - epoch_iter = tqdm(range(num_batch)) - for b in epoch_iter: - if b % 100 == 0: - epoch_iter.set_description( - # f"Progress: {b * 1.0 / num_batch * 100:.4f}, alpha: {self.alpha:.6f}, time: {time.time() - t0:.4f}" - ) - self.alpha = self.init_alpha * max((1 - b * 1.0 / num_batch), 0.0001) - u, v = [0] * batch_size, [0] * batch_size - for i in range(batch_size): - edge_id = alias_draw(self.edges_table, self.edges_prob) - u[i], v[i] = self.edges[edge_id] - if not self.is_directed and np.random.rand() > 0.5: - v[i], u[i] = self.edges[edge_id] - - vec_error = np.zeros((batch_size, self.dimension)) - label, target = np.asarray([1 for i in range(batch_size)]), np.asarray(v) - for j in range(1 + self.negative): - if j != 0: - label = np.asarray([0 for i in range(batch_size)]) - for i in range(batch_size): - target[i] = alias_draw(self.node_table, self.node_prob) - if order == 1: - self._update( - self.emb_vertex[u], self.emb_vertex[target], vec_error, label - ) - else: - self._update( - self.emb_vertex[u], self.emb_context[target], vec_error, label - ) - self.emb_vertex[u] += vec_error
- - -if __name__ == "__main__": - dataset = eg.CiteseerGraphDataset( - force_reload=True - ) # Download CiteseerGraphDataset contained in EasyGraph - num_classes = dataset.num_classes - g = dataset[0] - labels = g.ndata["label"] - edge_list = [] - for i in g.edges: - edge_list.append((i[0], i[1])) - g1 = eg.Graph() - g1.add_edges_from(edge_list) - # print(g.edges) - # print(g.__dir__()) - - model = LINE( - dimension=128, - walk_length=80, - walk_num=20, - negative=5, - batch_size=128, - init_alpha=0.025, - order=3, - ) - print(model) - - model.train() - out = model(g1, return_dict=True) - - keylist = sorted(out) - tmp = torch.cat( - ( - torch.unsqueeze(torch.tensor(out[keylist[0]]), -2), - torch.unsqueeze(torch.tensor(out[keylist[1]]), -2), - ), - 0, - ) - - for i in range(2, len(keylist)): - tmp = torch.cat((tmp, torch.unsqueeze(torch.tensor(out[keylist[i]]), -2)), 0) - torch.save(tmp, "line.emb") - print(tmp, tmp.shape) - - line_emb = [] - for i in range(0, len(tmp)): - line_emb.append(list(tmp[i])) - line_emb = np.array(line_emb) - -# tsne = TSNE(n_components=2) -# z = tsne.fit_transform(line_emb) -# z_data = np.vstack((z.T, labels)).T -# df_tsne = pd.DataFrame(z_data, columns=['Dim1', 'Dim2', 'class']) -# df_tsne['class'] = df_tsne['class'].astype(int) -# df_tsne.head() -# -# plt.figure(figsize=(8, 8)) -# sns.scatterplot(data=df_tsne, hue='class', x='Dim1', y='Dim2', palette=['green','orange','brown','red', 'blue','black']) -# plt.savefig('torch_line_citeseer.pdf', bbox_inches='tight') -# plt.show() -# -# -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/graph_embedding/node2vec.html b/docs/_modules/easygraph/functions/graph_embedding/node2vec.html deleted file mode 100644 index fabbe6fe..00000000 --- a/docs/_modules/easygraph/functions/graph_embedding/node2vec.html +++ /dev/null @@ -1,421 +0,0 @@ - - - - - - easygraph.functions.graph_embedding.node2vec — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.graph_embedding.node2vec

-import random
-
-import numpy as np
-
-from easygraph.utils import *
-from tqdm import tqdm
-
-
-__all__ = ["node2vec"]
-
-
-
[docs]@not_implemented_for("multigraph") -def node2vec( - G, - dimensions=128, - walk_length=80, - num_walks=10, - p=1.0, - q=1.0, - weight_key=None, - workers=None, - **skip_gram_params, -): - """Graph embedding via Node2Vec. - - Parameters - ---------- - G : easygraph.Graph or easygraph.DiGraph - - dimensions : int - Embedding dimensions, optional(default: 128) - - walk_length : int - Number of nodes in each walk, optional(default: 80) - - num_walks : int - Number of walks per node, optional(default: 10) - - p : float - The return hyper parameter, optional(default: 1.0) - - q : float - The input parameter, optional(default: 1.0) - - weight_key : string or None (default: None) - On weighted graphs, this is the key for the weight attribute - - workers : int or None, optional(default : None) - The number of workers generating random walks (default: None). None if not using only one worker. - - skip_gram_params : dict - Parameters for gensim.models.Word2Vec - do not supply 'size', it is taken from the 'dimensions' parameter - - Returns - ------- - embedding_vector : dict - The embedding vector of each node - - most_similar_nodes_of_node : dict - The most similar nodes of each node and its similarity - - Examples - -------- - - >>> node2vec(G, - ... dimensions=128, # The graph embedding dimensions. - ... walk_length=80, # Walk length of each random walks. - ... num_walks=10, # Number of random walks. - ... p=1.0, # The `p` possibility in random walk in [1]_ - ... q=1.0, # The `q` possibility in random walk in [1]_ - ... weight_key='weight', - ... skip_gram_params=dict( # The skip_gram parameters in Python package gensim. - ... window=10, - ... min_count=1, - ... batch_words=4 - ... )) - - References - ---------- - .. [1] https://arxiv.org/abs/1607.00653 - - """ - G_index, index_of_node, node_of_index = G.to_index_node_graph() - - if workers is None: - walks = simulate_walks( - G_index, - walk_length=walk_length, - num_walks=num_walks, - p=p, - q=q, - weight_key=weight_key, - ) - else: - from joblib import Parallel - from joblib import delayed - - num_walks_lists = np.array_split(range(num_walks), workers) - walks = Parallel(n_jobs=workers)( - delayed(simulate_walks)( - G_index, walk_length, len(num_walks), p, q, weight_key - ) - for num_walks in num_walks_lists - ) - # Change multidimensional array to one dimensional array - walks = [walk for walk_group in walks for walk in walk_group] - - model = learn_embeddings(walks=walks, dimensions=dimensions, **skip_gram_params) - - ( - embedding_vector, - most_similar_nodes_of_node, - ) = _get_embedding_result_from_gensim_skipgram_model( - G=G, index_of_node=index_of_node, node_of_index=node_of_index, model=model - ) - - del G_index - return embedding_vector, most_similar_nodes_of_node
- - -def _get_embedding_result_from_gensim_skipgram_model( - G, index_of_node, node_of_index, model -): - embedding_vector = dict() - most_similar_nodes_of_node = dict() - - def change_string_to_node_from_gensim_return_value(value_including_str): - # As the return value of gensim model.wv.most_similar includes string index in G_index, - # the string index should be changed to the original node element in G. - result = [] - for node_index, value in value_including_str: - node_index = int(node_index) - node = node_of_index[node_index] - result.append((node, value)) - return result - - for node in G.nodes: - # Output node names are always strings in gensim - embedding_vector[node] = model.wv[str(index_of_node[node])] - - most_similar_nodes = model.wv.most_similar(str(index_of_node[node])) - most_similar_nodes_of_node[ - node - ] = change_string_to_node_from_gensim_return_value(most_similar_nodes) - - return embedding_vector, most_similar_nodes_of_node - - -def simulate_walks(G, walk_length, num_walks, p, q, weight_key=None): - alias_nodes, alias_edges = _preprocess_transition_probs(G, p, q, weight_key) - walks = [] - nodes = list(G.nodes) - for walk_iter in tqdm(range(num_walks)): - random.shuffle(nodes) - for node in nodes: - walks.append( - _node2vec_walk( - G, - walk_length=walk_length, - start_node=node, - alias_nodes=alias_nodes, - alias_edges=alias_edges, - ) - ) - - return walks - - -def _preprocess_transition_probs(G, p, q, weight_key=None): - is_directed = G.is_directed() - alias_nodes = {} - - for node in G.nodes: - if weight_key is None: - unnormalized_probs = [1.0 for nbr in sorted(G.neighbors(node))] - else: - unnormalized_probs = [ - G[node][nbr][weight_key] for nbr in sorted(G.neighbors(node)) - ] - norm_const = sum(unnormalized_probs) - normalized_probs = [float(u_prob) / norm_const for u_prob in unnormalized_probs] - alias_nodes[node] = _alias_setup(normalized_probs) - - alias_edges = {} - triads = {} - - if is_directed: - for edge in G.edges: - alias_edges[(edge[0], edge[1])] = _get_alias_edge( - G, edge[0], edge[1], p, q, weight_key - ) - else: - for edge in G.edges: - alias_edges[(edge[0], edge[1])] = _get_alias_edge( - G, edge[0], edge[1], p, q, weight_key - ) - alias_edges[(edge[1], edge[0])] = _get_alias_edge( - G, edge[1], edge[0], p, q, weight_key - ) - - return alias_nodes, alias_edges - - -def _get_alias_edge(G, src, dst, p, q, weight_key=None): - unnormalized_probs = [] - - if weight_key is None: - for dst_nbr in sorted(G.neighbors(dst)): - if dst_nbr == src: - unnormalized_probs.append(1.0 / p) - elif G.has_edge(dst_nbr, src): - unnormalized_probs.append(1.0) - else: - unnormalized_probs.append(1.0 / q) - else: - for dst_nbr in sorted(G.neighbors(dst)): - if dst_nbr == src: - unnormalized_probs.append(G[dst][dst_nbr][weight_key] / p) - elif G.has_edge(dst_nbr, src): - unnormalized_probs.append(G[dst][dst_nbr][weight_key]) - else: - unnormalized_probs.append(G[dst][dst_nbr][weight_key] / q) - - norm_const = sum(unnormalized_probs) - normalized_probs = [float(u_prob) / norm_const for u_prob in unnormalized_probs] - - return _alias_setup(normalized_probs) - - -def _alias_setup(probs): - K = len(probs) - q = np.zeros(K) - J = np.zeros(K, dtype=int) - - smaller = [] - larger = [] - for kk, prob in enumerate(probs): - q[kk] = K * prob - if q[kk] < 1.0: - smaller.append(kk) - else: - larger.append(kk) - - while len(smaller) > 0 and len(larger) > 0: - small = smaller.pop() - large = larger.pop() - - J[small] = large - q[large] = q[large] + q[small] - 1.0 - if q[large] < 1.0: - smaller.append(large) - else: - larger.append(large) - - return J, q - - -def _node2vec_walk(G, walk_length, start_node, alias_nodes, alias_edges): - """ - Simulate a random walk starting from start node. - """ - walk = [start_node] - - while len(walk) < walk_length: - cur = walk[-1] - cur_nbrs = sorted(G.neighbors(cur)) - if len(cur_nbrs) > 0: - if len(walk) == 1: - walk.append( - cur_nbrs[_alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])] - ) - else: - prev = walk[-2] - next_node = cur_nbrs[ - _alias_draw( - alias_edges[(prev, cur)][0], alias_edges[(prev, cur)][1] - ) - ] - walk.append(next_node) - else: - break - - return walk - - -def _alias_draw(J, q): - K = len(J) - kk = int(np.floor(np.random.rand() * K)) - if np.random.rand() < q[kk]: - return kk - else: - return J[kk] - - -def learn_embeddings(walks, dimensions, **skip_gram_params): - """ - Learn embeddings with Word2Vec. - """ - from gensim.models import Word2Vec - - walks = [list(map(str, walk)) for walk in walks] - - if "vector_size" not in skip_gram_params: - skip_gram_params["vector_size"] = dimensions - - model = Word2Vec(walks, **skip_gram_params) - - return model -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/graph_embedding/sdne.html b/docs/_modules/easygraph/functions/graph_embedding/sdne.html deleted file mode 100644 index 8c991ef3..00000000 --- a/docs/_modules/easygraph/functions/graph_embedding/sdne.html +++ /dev/null @@ -1,393 +0,0 @@ - - - - - - easygraph.functions.graph_embedding.sdne — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.graph_embedding.sdne

-from argparse import ArgumentDefaultsHelpFormatter
-from argparse import ArgumentParser
-
-import numpy as np
-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-import torch.optim as optim
-
-from torch.utils import data
-from torch.utils.data.dataloader import DataLoader
-
-
-
[docs]def parse_args(): - parser = ArgumentParser( - formatter_class=ArgumentDefaultsHelpFormatter, conflict_handler="resolve" - ) - parser.add_argument( - "--output", default="node.emb", help="Output representation file" - ) - parser.add_argument( - "--workers", default=8, type=int, help="Number of parallel processes." - ) - parser.add_argument( - "--weighted", action="store_true", default=False, help="Treat graph as weighted" - ) - parser.add_argument( - "--epochs", default=400, type=int, help="The training epochs of SDNE" - ) - parser.add_argument( - "--dropout", - default=0.05, - type=float, - help="Dropout rate (1 - keep probability)", - ) - parser.add_argument( - "--weight-decay", - type=float, - default=5e-4, - help="Weight for L2 loss on embedding matrix", - ) - parser.add_argument("--lr", default=0.006, type=float, help="learning rate") - parser.add_argument( - "--alpha", default=1e-2, type=float, help="alhpa is a hyperparameter in SDNE" - ) - parser.add_argument( - "--beta", default=5.0, type=float, help="beta is a hyperparameter in SDNE" - ) - parser.add_argument( - "--nu1", default=1e-5, type=float, help="nu1 is a hyperparameter in SDNE" - ) - parser.add_argument( - "--nu2", default=1e-4, type=float, help="nu2 is a hyperparameter in SDNE" - ) - parser.add_argument("--bs", default=100, type=int, help="batch size of SDNE") - parser.add_argument("--nhid0", default=1000, type=int, help="The first dim") - parser.add_argument("--nhid1", default=128, type=int, help="The second dim") - parser.add_argument( - "--step_size", default=10, type=int, help="The step size for lr" - ) - parser.add_argument("--gamma", default=0.9, type=int, help="The gamma for lr") - args = parser.parse_args() - - return args
- - -
[docs]class Dataload(data.Dataset): - def __init__(self, Adj, Node): - self.Adj = Adj - self.Node = Node - - def __getitem__(self, index): - return index - # adj_batch = self.Adj[index] - # adj_mat = adj_batch[index] - # b_mat = torch.ones_like(adj_batch) - # b_mat[adj_batch != 0] = self.Beta - # return adj_batch, adj_mat, b_mat - - def __len__(self): - return self.Node
- - -
[docs]def get_adj(g): - edges = list(g.edges) - edges = [(edges[i][0], edges[i][1]) for i in range(len(edges))] - # print(edges) - edges = np.array([np.array(i) for i in edges]) - min_node, max_node = edges.min(), edges.max() - if min_node == 0: - Node = max_node + 1 - else: - Node = max_node - - Adj = np.zeros([Node, Node], dtype=int) - for i in range(edges.shape[0]): - g.add_edge(edges[i][0], edges[i][1]) - if min_node == 0: - Adj[edges[i][0], edges[i][1]] = 1 - Adj[edges[i][1], edges[i][0]] = 1 - - else: - Adj[edges[i][0] - 1, edges[i][1] - 1] = 1 - Adj[edges[i][1] - 1, edges[i][0] - 1] = 1 - Adj = torch.FloatTensor(Adj) - return Adj, Node
- - -
[docs]class SDNE(nn.Module): - """ - Graph embedding via SDNE. - - Parameters - ---------- - graph : easygraph.Graph or easygraph.DiGraph - - node: Size of nodes - - nhid0, nhid1: Two dimensions of two hiddenlayers, default: 128, 64 - - dropout: One parameter for regularization, default: 0.025 - - alpha, beta: Twe parameters - graph=g: : easygraph.Graph or easygraph.DiGraph - - Examples - -------- - >>> import easygraph as eg - >>> model = eg.SDNE(graph=g, node_size= len(g.nodes), nhid0=128, nhid1=64, dropout=0.025, alpha=2e-2, beta=10) - >>> emb = model.train(model, epochs, lr, bs, step_size, gamma, nu1, nu2, device, output) - - - epochs, "--epochs", default=400, type=int, help="The training epochs of SDNE" - - alpha, "--alpha", default=2e-2, type=float, help="alhpa is a hyperparameter in SDNE" - - beta, "--beta", default=10.0, type=float, help="beta is a hyperparameter in SDNE" - - lr, "--lr", default=0.006, type=float, help="learning rate" - - bs, "--bs", default=100, type=int, help="batch size of SDNE" - - step_size, "--step_size", default=10, type=int, help="The step size for lr" - - gamma, # "--gamma", default=0.9, type=int, help="The gamma for lr" - - step_size, "--step_size", default=10, type=int, help="The step size for lr" - - nu1, # "--nu1", default=1e-5, type=float, help="nu1 is a hyperparameter in SDNE" - - nu2, "--nu2", default=1e-4, type=float, help="nu2 is a hyperparameter in SDNE" - - device, "-- device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") " - - output "--output", default="node.emb", help="Output representation file" - - - Reference - ---------- - .. [1] Wang, D., Cui, P., & Zhu, W. (2016, August). Structural deep network embedding. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 1225-1234). - - https://www.kdd.org/kdd2016/papers/files/rfp0191-wangAemb.pdf - """ - - def __init__( - self, graph, node_size, nhid0, nhid1, dropout=0.06, alpha=2e-2, beta=10.0 - ): - super(SDNE, self).__init__() - self.encode0 = nn.Linear(node_size, nhid0) - self.encode1 = nn.Linear(nhid0, nhid1) - self.decode0 = nn.Linear(nhid1, nhid0) - self.decode1 = nn.Linear(nhid0, node_size) - self.droput = dropout - self.alpha = alpha - self.beta = beta - self.graph = graph - -
[docs] def forward(self, adj_batch, adj_mat, b_mat): - t0 = F.leaky_relu(self.encode0(adj_batch)) - t0 = F.leaky_relu(self.encode1(t0)) - embedding = t0 - t0 = F.leaky_relu(self.decode0(t0)) - t0 = F.leaky_relu(self.decode1(t0)) - embedding_norm = torch.sum(embedding * embedding, dim=1, keepdim=True) - L_1st = torch.sum( - adj_mat - * ( - embedding_norm - - 2 * torch.mm(embedding, torch.transpose(embedding, dim0=0, dim1=1)) - + torch.transpose(embedding_norm, dim0=0, dim1=1) - ) - ) - L_2nd = torch.sum(((adj_batch - t0) * b_mat) * ((adj_batch - t0) * b_mat)) - return L_1st, self.alpha * L_2nd, L_1st + self.alpha * L_2nd
- -
[docs] def train( - self, - model, - epochs=100, - lr=0.006, - bs=100, - step_size=10, - gamma=0.9, - nu1=1e-5, - nu2=1e-4, - device="cpu", - output="out.emb", - ): - Adj, Node = get_adj(self.graph) - model = model.to(device) - - opt = optim.Adam(model.parameters(), lr=lr) - scheduler = torch.optim.lr_scheduler.StepLR( - opt, step_size=step_size, gamma=gamma - ) - Data = Dataload(Adj, Node) - Data = DataLoader( - Data, - batch_size=bs, - shuffle=True, - ) - - for epoch in range(1, epochs + 1): - loss_sum, loss_L1, loss_L2, loss_reg = 0, 0, 0, 0 - for index in Data: - adj_batch = Adj[index] - adj_mat = adj_batch[:, index] - b_mat = torch.ones_like(adj_batch) - b_mat[adj_batch != 0] = self.beta - - opt.zero_grad() - L_1st, L_2nd, L_all = model(adj_batch, adj_mat, b_mat) - L_reg = 0 - for param in model.parameters(): - L_reg += nu1 * torch.sum(torch.abs(param)) + nu2 * torch.sum( - param * param - ) - Loss = L_all + L_reg - Loss.backward() - opt.step() - loss_sum += Loss - loss_L1 += L_1st - loss_L2 += L_2nd - loss_reg += L_reg - scheduler.step(epoch) - # print("The lr for epoch %d is %f" %(epoch, scheduler.get_lr()[0])) - print("loss for epoch %d is:" % epoch) - print("loss_sum is %f" % loss_sum) - print("loss_L1 is %f" % loss_L1) - print("loss_L2 is %f" % loss_L2) - print("loss_reg is %f" % loss_reg) - - # model.eval() - embedding = model.savector(Adj) - outVec = embedding.detach().numpy() - np.savetxt(output, outVec) - - return outVec
- -
[docs] def savector(self, adj): - t0 = self.encode0(adj) - t0 = self.encode1(t0) - return t0
- - -# if __name__ == '__main__': -# args = parse_args() -# print(args) -# dataset = eg.CiteseerGraphDataset(force_reload=True) # Download CiteseerGraphDataset contained in EasyGraph -# num_classes = dataset.num_classes -# g = dataset[0] -# print(g) -# device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") -# adj, node = get_adj(g) -# # labels = g.ndata['label'] -# nhid0, nhid1, dropout, alpha = args.nhid0, args.nhid1, args.dropout, args.alpha -# model = SDNE(node, nhid0, nhid1, dropout, alpha, graph=g) -# print(model) -# -# emb = model.train(args, device) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/graph_generator/RandomNetwork.html b/docs/_modules/easygraph/functions/graph_generator/RandomNetwork.html deleted file mode 100644 index dfdb7e21..00000000 --- a/docs/_modules/easygraph/functions/graph_generator/RandomNetwork.html +++ /dev/null @@ -1,484 +0,0 @@ - - - - - - easygraph.functions.graph_generator.RandomNetwork — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.graph_generator.RandomNetwork

-import math
-import random
-
-import easygraph as eg
-
-
-__all__ = [
-    "erdos_renyi_M",
-    "erdos_renyi_P",
-    "fast_erdos_renyi_P",
-    "WS_Random",
-]
-
-
-
[docs]def erdos_renyi_M(n, edge, directed=False, FilePath=None): - """Given the number of nodes and the number of edges, return an Erdős-Rényi random graph, and store the graph in a document. - - Parameters - ---------- - n : int - The number of nodes. - edge : int - The number of edges. - directed : bool, optional (default=False) - If True, this function returns a directed graph. - FilePath : string - The file for storing the output graph G. - - Returns - ------- - G : graph - an Erdős-Rényi random graph. - - Examples - -------- - Returns an Erdős-Rényi random graph G. - - >>> erdos_renyi_M(100,180,directed=False,FilePath="/users/fudanmsn/downloads/RandomNetwork.txt") - - References - ---------- - .. [1] P. Erdős and A. Rényi, On Random Graphs, Publ. Math. 6, 290 (1959). - .. [2] E. N. Gilbert, Random Graphs, Ann. Math. Stat., 30, 1141 (1959). - """ - if directed: - G = eg.DiGraph() - adjacent = {} - mmax = n * (n - 1) - if edge >= mmax: - for i in range(n): - for j in range(n): - if i != j: - G.add_edge(i, j) - if i not in adjacent: - adjacent[i] = [] - adjacent[i].append(j) - else: - adjacent[i].append(j) - return G - count = 0 - while count < edge: - i = random.randint(0, n - 1) - j = random.randint(0, n - 1) - if i == j or G.has_edge(i, j): - continue - else: - count = count + 1 - if i not in adjacent: - adjacent[i] = [] - adjacent[i].append(j) - else: - adjacent[i].append(j) - G.add_edge(i, j) - else: - G = eg.Graph() - adjacent = {} - mmax = n * (n - 1) / 2 - if edge >= mmax: - for i in range(n): - for j in range(n): - if i != j: - G.add_edge(i, j) - if i not in adjacent: - adjacent[i] = [] - adjacent[i].append(j) - else: - adjacent[i].append(j) - if j not in adjacent: - adjacent[j] = [] - adjacent[j].append(i) - else: - adjacent[j].append(i) - return G - count = 0 - while count < edge: - i = random.randint(0, n - 1) - j = random.randint(0, n - 1) - if i == j or G.has_edge(i, j): - continue - else: - count = count + 1 - if i not in adjacent: - adjacent[i] = [] - adjacent[i].append(j) - else: - adjacent[i].append(j) - if j not in adjacent: - adjacent[j] = [] - adjacent[j].append(i) - else: - adjacent[j].append(i) - G.add_edge(i, j) - - writeRandomNetworkToFile(n, adjacent, FilePath) - return G
- - -
[docs]def erdos_renyi_P(n, p, directed=False, FilePath=None): - """Given the number of nodes and the probability of edge creation, return an Erdős-Rényi random graph, and store the graph in a document. - - Parameters - ---------- - n : int - The number of nodes. - p : float - Probability for edge creation. - directed : bool, optional (default=False) - If True, this function returns a directed graph. - FilePath : string - The file for storing the output graph G. - - Returns - ------- - G : graph - an Erdős-Rényi random graph. - - Examples - -------- - Returns an Erdős-Rényi random graph G - - >>> erdos_renyi_P(100,0.5,directed=False,FilePath="/users/fudanmsn/downloads/RandomNetwork.txt") - - References - ---------- - .. [1] P. Erdős and A. Rényi, On Random Graphs, Publ. Math. 6, 290 (1959). - .. [2] E. N. Gilbert, Random Graphs, Ann. Math. Stat., 30, 1141 (1959). - """ - if directed: - G = eg.DiGraph() - adjacent = {} - probability = 0.0 - for i in range(n): - for j in range(i + 1, n): - probability = random.random() - if probability < p: - if i not in adjacent: - adjacent[i] = [] - adjacent[i].append(j) - else: - adjacent[i].append(j) - G.add_edge(i, j) - else: - G = eg.Graph() - adjacent = {} - probability = 0.0 - for i in range(n): - for j in range(i + 1, n): - probability = random.random() - if probability < p: - if i not in adjacent: - adjacent[i] = [] - adjacent[i].append(j) - else: - adjacent[i].append(j) - if j not in adjacent: - adjacent[j] = [] - adjacent[j].append(i) - else: - adjacent[j].append(i) - G.add_edge(i, j) - - writeRandomNetworkToFile(n, adjacent, FilePath) - return G
- - -
[docs]def fast_erdos_renyi_P(n, p, directed=False, FilePath=None): - """Given the number of nodes and the probability of edge creation, return an Erdős-Rényi random graph, and store the graph in a document. Use this function for generating a huge scale graph. - - Parameters - ---------- - n : int - The number of nodes. - p : float - Probability for edge creation. - directed : bool, optional (default=False) - If True, this function returns a directed graph. - FilePath : string - The file for storing the output graph G. - - Returns - ------- - G : graph - an Erdős-Rényi random graph. - - Examples - -------- - Returns an Erdős-Rényi random graph G - - >>> erdos_renyi_P(100,0.5,directed=False,FilePath="/users/fudanmsn/downloads/RandomNetwork.txt") - - References - ---------- - .. [1] P. Erdős and A. Rényi, On Random Graphs, Publ. Math. 6, 290 (1959). - .. [2] E. N. Gilbert, Random Graphs, Ann. Math. Stat., 30, 1141 (1959). - """ - if directed: - G = eg.DiGraph() - w = -1 - lp = math.log(1.0 - p) - v = 0 - adjacent = {} - while v < n: - lr = math.log(1.0 - random.random()) - w = w + 1 + int(lr / lp) - if v == w: # avoid self loops - w = w + 1 - while v < n <= w: - w = w - n - v = v + 1 - if v == w: # avoid self loops - w = w + 1 - if v < n: - G.add_edge(v, w) - if v not in adjacent: - adjacent[v] = [] - adjacent[v].append(w) - else: - adjacent[v].append(w) - else: - G = eg.Graph() - w = -1 - lp = math.log(1.0 - p) - v = 1 - adjacent = {} - while v < n: - lr = math.log(1.0 - random.random()) - w = w + 1 + int(lr / lp) - while w >= v and v < n: - w = w - v - v = v + 1 - if v < n: - G.add_edge(v, w) - if v not in adjacent: - adjacent[v] = [] - adjacent[v].append(w) - else: - adjacent[v].append(w) - if w not in adjacent: - adjacent[w] = [] - adjacent[w].append(v) - else: - adjacent[w].append(v) - - writeRandomNetworkToFile(n, adjacent, FilePath) - return G
- - -
[docs]def WS_Random(n, k, p, FilePath=None): - """Returns a small-world graph. - - Parameters - ---------- - n : int - The number of nodes - k : int - Each node is joined with its `k` nearest neighbors in a ring - topology. - p : float - The probability of rewiring each edge - FilePath : string - The file for storing the output graph G - - Returns - ------- - G : graph - a small-world graph - - Examples - -------- - Returns a small-world graph G - - >>> WS_Random(100,10,0.3,"/users/fudanmsn/downloads/RandomNetwork.txt") - - """ - if k >= n: - print("k>=n, choose smaller k or larger n") - return - adjacent = {} - G = eg.Graph() - NUM1 = n - NUM2 = NUM1 - 1 - K = k - K1 = K + 1 - N = list(range(NUM1)) - G.add_nodes(N) - - for i in range(NUM1): - for j in range(1, K1): - K_add = NUM1 - K - i_add_j = i + j + 1 - if i >= K_add and i_add_j > NUM1: - i_add = i + j - NUM1 - G.add_edge(i, i_add) - else: - i_add = i + j - G.add_edge(i, i_add) - if i not in adjacent: - adjacent[i] = [] - adjacent[i].append(i_add) - else: - adjacent[i].append(i_add) - if i_add not in adjacent: - adjacent[i_add] = [] - adjacent[i_add].append(i) - else: - adjacent[i_add].append(i) - for i in range(NUM1): - for e_del in range(i + 1, i + K1): - if e_del >= NUM1: - e_del = e_del - NUM1 - P_random = random.random() - if P_random < p: - G.remove_edge(i, e_del) - adjacent[i].remove(e_del) - if adjacent[i] == []: - adjacent.pop(i) - adjacent[e_del].remove(i) - if adjacent[e_del] == []: - adjacent.pop(e_del) - e_add = random.randint(0, NUM2) - while e_add == i or G.has_edge(i, e_add) == True: - e_add = random.randint(0, NUM2) - G.add_edge(i, e_add) - if i not in adjacent: - adjacent[i] = [] - adjacent[i].append(e_add) - else: - adjacent[i].append(e_add) - if e_add not in adjacent: - adjacent[e_add] = [] - adjacent[e_add].append(i) - else: - adjacent[e_add].append(i) - writeRandomNetworkToFile(n, adjacent, FilePath) - return G
- - -def writeRandomNetworkToFile(n, adjacent, FilePath): - if FilePath != None: - f = open(FilePath, "w+") - else: - f = open("RandomNetwork.txt", "w+") - adjacent = sorted(adjacent.items(), key=lambda d: d[0]) - for i in adjacent: - i[1].sort() - for j in i[1]: - f.write(str(i[0])) - f.write(" ") - f.write(str(j)) - f.write("\n") - f.close() -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/graph_generator/classic.html b/docs/_modules/easygraph/functions/graph_generator/classic.html deleted file mode 100644 index 45307455..00000000 --- a/docs/_modules/easygraph/functions/graph_generator/classic.html +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - easygraph.functions.graph_generator.classic — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.graph_generator.classic

-import itertools
-
-from easygraph.classes import Graph
-from easygraph.utils import nodes_or_number
-from easygraph.utils import pairwise
-
-
-__all__ = ["empty_graph", "path_graph", "complete_graph"]
-
-
-
[docs]@nodes_or_number(0) -def empty_graph(n=0, create_using=None, default=Graph): - if create_using is None: - G = default() - elif hasattr(create_using, "_adj"): - # create_using is a EasyGraph style Graph - G = create_using - else: - # try create_using as constructor - G = create_using() - - n_name, nodes = n - G.add_nodes_from(nodes) - return G
- - -
[docs]@nodes_or_number(0) -def path_graph(n, create_using=None): - n_name, nodes = n - G = empty_graph(nodes, create_using) - G.add_edges_from(pairwise(nodes)) - return G
- - -
[docs]@nodes_or_number(0) -def complete_graph(n, create_using=None): - """Return the complete graph `K_n` with n nodes. - - A complete graph on `n` nodes means that all pairs - of distinct nodes have an edge connecting them. - - Parameters - ---------- - n : int or iterable container of nodes - If n is an integer, nodes are from range(n). - If n is a container of nodes, those nodes appear in the graph. - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - - Examples - -------- - >>> G = eg.complete_graph(9) - >>> len(G) - 9 - >>> G.size() - 36 - >>> G = eg.complete_graph(range(11, 14)) - >>> list(G.nodes()) - [11, 12, 13] - >>> G = eg.complete_graph(4, eg.DiGraph()) - >>> G.is_directed() - True - - """ - n_name, nodes = n - G = empty_graph(n_name, create_using) - if len(nodes) > 1: - if G.is_directed(): - edges = itertools.permutations(nodes, 2) - else: - edges = itertools.combinations(nodes, 2) - G.add_edges_from(edges) - return G
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/assortativity.html b/docs/_modules/easygraph/functions/hypergraph/assortativity.html deleted file mode 100644 index c30839d3..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/assortativity.html +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - easygraph.functions.hypergraph.assortativity — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.assortativity

-"""Algorithms for finding the degree assortativity of a hypergraph."""
-
-import random
-
-from itertools import combinations
-
-import numpy
-import numpy as np
-
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = ["dynamical_assortativity", "degree_assortativity"]
-
-
-
[docs]def dynamical_assortativity(H): - """Computes the dynamical assortativity of a uniform hypergraph. - - Parameters - ---------- - H : eg.Hypergraph - Hypergraph of interest - - Returns - ------- - float - The dynamical assortativity - - See Also - -------- - degree_assortativity - - Raises - ------ - EasyGraphError - If the hypergraph is not uniform, or if there are no nodes - or no edges - - References - ---------- - Nicholas Landry and Juan G. Restrepo, - Hypergraph assortativity: A dynamical systems perspective, - Chaos 2022. - DOI: 10.1063/5.0086905 - - """ - if len(H.v) == 0: - raise EasyGraphError("Hypergraph must contain nodes") - elif len(H.e[0]) == 0: - raise EasyGraphError("Hypergraph must contain edges!") - - if not H.is_uniform(): - raise EasyGraphError("Hypergraph must be uniform!") - - if 1 in H.unique_edge_sizes(): - raise EasyGraphError("No singleton edges!") - - degs = H.deg_v - k1 = sum(degs) / len(degs) - k2 = np.mean(numpy.array(degs) ** 2) - kk1 = np.mean( - [degs[n1] * degs[n2] for e in H.e[0] for n1, n2 in combinations(e, 2)] - ) - - return kk1 * k1**2 / k2**2 - 1
- - -
[docs]def degree_assortativity(H, kind="uniform", exact=False, num_samples=1000): - """Computes the degree assortativity of a hypergraph - - Parameters - ---------- - H : Hypergraph - The hypergraph of interest - kind : str, optional - the type of degree assortativity. valid choices are - "uniform", "top-2", and "top-bottom". By default, "uniform". - exact : bool, optional - whether to compute over all edges or sample randomly from the - set of edges. By default, False. - num_samples : int, optional - if not exact, specify the number of samples for the computation. - By default, 1000. - - Returns - ------- - float - the degree assortativity - - Raises - ------ - EasyGraphError - If there are no nodes or no edges - - See Also - -------- - dynamical_assortativity - - References - ---------- - Phil Chodrow, - Configuration models of random hypergraphs, - Journal of Complex Networks 2020. - DOI: 10.1093/comnet/cnaa018 - """ - - if len(H.v) == 0: - raise EasyGraphError("Hypergraph must contain nodes") - elif len(H.e[0]) == 0: - raise EasyGraphError("Hypergraph must contain edges!") - - degs = H.deg_v - if exact: - k1k2 = [_choose_degrees(e, degs, kind) for e in H.e[0] if len(e) > 1] - else: - edges = [e for e in H.e[0] if len(e) > 1] - k1k2 = [ - _choose_degrees(random.choice(H.e[0]), degs, kind) - for _ in range(num_samples) - ] - - rho = np.corrcoef(np.array(k1k2).T)[0, 1] - if np.isnan(rho): - return 0 - return rho
- - -def _choose_degrees(e, k, kind="uniform"): - """Choose the degrees of two nodes in a hyperedge. - - Parameters - ---------- - e : iterable - the members in a hyperedge - k : dict - the degrees where keys are node IDs and values are degrees - kind : str, optional - the type of degree assortativity, options are "uniform", "top-2", - and "top-bottom". By default, "uniform". - - Returns - ------- - tuple - two degrees selected from the edge - - Raises - ------ - EasyGraphError - if invalid assortativity function chosen - - See Also - -------- - degree_assortativity - - References - ---------- - Phil Chodrow, - Configuration models of random hypergraphs, - Journal of Complex Networks 2020. - DOI: 10.1093/comnet/cnaa018 - """ - e = list(e) - if len(e) > 1: - if kind == "uniform": - i = np.random.randint(len(e)) - j = i - while i == j: - j = np.random.randint(len(e)) - return (k[e[i]], k[e[j]]) - - elif kind == "top-2": - degs = sorted([k[i] for i in e])[-2:] - random.shuffle(degs) - return degs - - elif kind == "top-bottom": - # this selects the largest and smallest degrees in one line - degs = sorted([k[i] for i in e])[:: len(e) - 1] - random.shuffle(degs) - return degs - - else: - raise EasyGraphError("Invalid choice function!") - else: - raise EasyGraphError("Edge must have more than one member!") -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/centrality/cycle_ratio.html b/docs/_modules/easygraph/functions/hypergraph/centrality/cycle_ratio.html deleted file mode 100644 index 29560d0d..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/centrality/cycle_ratio.html +++ /dev/null @@ -1,306 +0,0 @@ - - - - - - easygraph.functions.hypergraph.centrality.cycle_ratio — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.centrality.cycle_ratio

-import copy
-import itertools
-
-import easygraph as eg
-
-
-__all__ = [
-    "my_all_shortest_paths",
-    "getandJudgeSimpleCircle",
-    "getSmallestCycles",
-    "StatisticsAndCalculateIndicators",
-    "cycle_ratio_centrality",
-]
-
-
-
[docs]def my_all_shortest_paths(G, source, target): - pred = eg.predecessor(G, source) - if target not in pred: - raise eg.EasyGraphNoPath( - f"Target {target} cannot be reached from given sources" - ) - sources = {source} - seen = {target} - stack = [[target, 0]] - top = 0 - while top >= 0: - node, i = stack[top] - if node in sources: - yield [p for p, n in reversed(stack[: top + 1])] - if len(pred[node]) > i: - stack[top][1] = i + 1 - next = pred[node][i] - if next in seen: - continue - else: - seen.add(next) - top += 1 - if top == len(stack): - stack.append([next, 0]) - else: - stack[top][:] = [next, 0] - else: - seen.discard(node) - top -= 1
- - -
[docs]def getandJudgeSimpleCircle(objectList, G): # 这里添加 G 作为参数 - numEdge = 0 - for eleArr in list(itertools.combinations(objectList, 2)): - if G.has_edge(eleArr[0], eleArr[1]): - numEdge += 1 - if numEdge != len(objectList): - return False - else: - return True
- - -
[docs]def getSmallestCycles(G, NodeGirth, Coreness, DEF_IMPOSSLEN): - NodeList = list(G.nodes) - NodeList.sort() - # setp 1 - curCyc = list() - for ix in NodeList[:-2]: # v1 - if NodeGirth[ix] == 0: - continue - curCyc.append(ix) - for jx in NodeList[NodeList.index(ix) + 1 : -1]: # v2 - if NodeGirth[jx] == 0: - continue - curCyc.append(jx) - if G.has_edge(ix, jx): - for kx in NodeList[NodeList.index(jx) + 1 :]: # v3 - if NodeGirth[kx] == 0: - continue - if G.has_edge(kx, ix): - curCyc.append(kx) - if G.has_edge(kx, jx): - yield tuple(curCyc) # 这里改为 yield - for i in curCyc: - NodeGirth[i] = 3 - curCyc.pop() - curCyc.pop() - curCyc.pop() - - # setp 2 - ResiNodeList = [] # Residual Node List - for nod in NodeList: - if NodeGirth[nod] == DEF_IMPOSSLEN: - ResiNodeList.append(nod) - if len(ResiNodeList) == 0: - return - else: - visitedNodes = dict.fromkeys(ResiNodeList, set()) - for nod in ResiNodeList: - if Coreness[nod] == 2 and NodeGirth[nod] < DEF_IMPOSSLEN: - continue - for nei in list(G.neighbors(nod)): - if Coreness[nei] == 2 and NodeGirth[nei] < DEF_IMPOSSLEN: - continue - if not nei in visitedNodes.keys() or not nod in visitedNodes[nei]: - visitedNodes[nod].add(nei) - if nei not in visitedNodes.keys(): - visitedNodes[nei] = set([nod]) - else: - visitedNodes[nei].add(nod) - if Coreness[nei] == 2 and NodeGirth[nei] < DEF_IMPOSSLEN: - continue - G.remove_edge(nod, nei) - if eg.single_source_dijkstra(G, nod, nei): - for path in my_all_shortest_paths(G, nod, nei): - lenPath = len(path) - path.sort() - yield tuple(path) # 这里改为 yield - for i in path: - if NodeGirth[i] > lenPath: - NodeGirth[i] = lenPath - G.add_edge(nod, nei)
- - -
[docs]def StatisticsAndCalculateIndicators(SmallestCyclesOfNodes, CycLenDict, SmallestCycles): - NumSmallCycles = len(SmallestCycles) - for cyc in SmallestCycles: - lenCyc = len(cyc) - CycLenDict[lenCyc] += 1 - for nod in cyc: - SmallestCyclesOfNodes[nod].add(cyc) - CycleRatio = {} # 这里将 CycleRatio 作为局部变量 - for objNode, SmaCycs in SmallestCyclesOfNodes.items(): - if len(SmaCycs) == 0: - continue - cycleNeighbors = set() - NeiOccurTimes = {} - for cyc in SmaCycs: - for n in cyc: - if n in NeiOccurTimes.keys(): - NeiOccurTimes[n] += 1 - else: - NeiOccurTimes[n] = 1 - cycleNeighbors = cycleNeighbors.union(cyc) - cycleNeighbors.remove(objNode) - del NeiOccurTimes[objNode] - sum = 0 - for nei in cycleNeighbors: - sum += float(NeiOccurTimes[nei]) / len(SmallestCyclesOfNodes[nei]) - CycleRatio[objNode] = sum + 1 - return CycleRatio
- - -
[docs]def cycle_ratio_centrality(G): - """ - Parameters - ---------- - G : eg.Graph - - Returns - ------- - cycle ratio centrality of each node in G : dict - - Example - ------- - >>> G = eg.Graph() - >>> G.add_edges([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4), (1, 5), (2, 5)]) - >>> cycle_ratio_centrality(G) - {1: 4.083333333333333, 2: 4.083333333333333, 3: 2.6666666666666665, 4: 2.6666666666666665, 5: 1.5} - - """ - NumNode = G.number_of_nodes() # update - DEF_IMPOSSLEN = NumNode + 1 # Impossible simple cycle length - NodeGirth = dict() - CycLenDict = dict() - - SmallestCyclesOfNodes = {} - removeNodes = set() - Coreness = dict(zip(list(G.nodes), eg.k_core(G))) - for i in list(G.nodes): - SmallestCyclesOfNodes[i] = set() - if G.degree()[i] <= 1 or Coreness[i] <= 1: - NodeGirth[i] = 0 - removeNodes.add(i) - else: - NodeGirth[i] = DEF_IMPOSSLEN - - G.remove_nodes_from(removeNodes) - - NodeNum = G.number_of_nodes() - for i in range(3, NodeNum + 2): - CycLenDict[i] = 0 - - SmallestCycles = set(getSmallestCycles(G, NodeGirth, Coreness, DEF_IMPOSSLEN)) - cycle_ratio = StatisticsAndCalculateIndicators( - SmallestCyclesOfNodes, CycLenDict, SmallestCycles - ) - return cycle_ratio
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/centrality/degree.html b/docs/_modules/easygraph/functions/hypergraph/centrality/degree.html deleted file mode 100644 index 82480b15..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/centrality/degree.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - easygraph.functions.hypergraph.centrality.degree — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • »
  • -
  • Module code »
  • -
  • easygraph.functions.hypergraph.centrality.degree
  • -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.centrality.degree

-__all__ = ["hyepergraph_degree_centrality"]
-
-
-
[docs]def hyepergraph_degree_centrality(G): - """ - - Parameters - ---------- - G : eg.Hypergraph - The target hypergraph - - Returns - ---------- - degree centrality of each node in G : dict - - """ - res = {} - node_list = G.v - # Get hyperedge list - edge_list = G.e[0] - for node in node_list: - res[node] = 0 - - for e in edge_list: - for n in e: - res[n] += 1 - - return res
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/centrality/hypercoreness.html b/docs/_modules/easygraph/functions/hypergraph/centrality/hypercoreness.html deleted file mode 100644 index d6f37fbd..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/centrality/hypercoreness.html +++ /dev/null @@ -1,464 +0,0 @@ - - - - - - easygraph.functions.hypergraph.centrality.hypercoreness — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.centrality.hypercoreness

-from itertools import compress
-
-import easygraph as eg
-import numpy as np
-
-
-__all__ = ["size_independent_hypercoreness", "frequency_based_hypercoreness"]
-
-
-
[docs]def size_independent_hypercoreness(h): - """The size_independent_hypercoreness of nodes in hypergraph. - - Parameters - ---------- - h : eg.Hypergraph. - - - Returns - ---------- - dict - Centrality, where keys are node IDs and values are lists of centralities. - - References - ---------- - Mancastroppa, M., Iacopini, I., Petri, G. et al. Hyper-cores promote localization and efficient seeding in higher-order processes. Nat Commun 14, 6223 (2023). https://doi.org/10.1038/s41467-023-41887-2. - - """ - e_list = h.e[0] - initial_node_num = h.num_v - data = [e_list[i] for i in range(len(e_list)) if len(e_list[i]) > 1] - - data.sort(key=len) - L = len(data) - size_max = len(data[L - 1]) - - size = list([len(data[j]) for j in range(L)]) - - X = eg.Hypergraph(num_v=initial_node_num, e_list=data) - IDX = list(range(0, X.num_v)) - - M = range(2, size_max + 1) - k_step = 1 - K = range(1, 1200, k_step) - k_shell_dict = {} - idx_orig = IDX - - IDX_size = range(len(size)) - k_max = np.zeros(len(M)) - - for j in idx_orig: - k_shell_dict[j] = np.zeros(len(M)) - - for x in range(len(M)): - m = M[x] - - D = np.zeros(len(K)) - - # consider only hyperedges of size >=m - idx_size = list( - compress(IDX_size, np.greater_equal(size, m * np.ones(len(size)))) - ) - int_sel = list([data[i] for i in idx_size]) - # build hypergraph with only interactions of size >=m - X = eg.Hypergraph(num_v=initial_node_num, e_list=int_sel) - node_set = set() - for sublist in int_sel: - for element in sublist: - node_set.add(element) - IDX = list(node_set) - # IDX_e = list(X.e[0]) - - for y in range(len(K)): - kk = K[y] - - d_tot_m = np.zeros(len(IDX)) - prev_shell = IDX - - for i in range(len(IDX)): - d_tot_m[i] = X.degree_node[IDX[i]] - - idx_n_remove = list( - compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m)) - ) # nodes with degree<k are removed - # X.remove_nodes_from(idx_n_remove) - now_e_list = X.e[0] - new_e_list = [] - for e in now_e_list: - new_e = [] - for n in e: - if n not in idx_n_remove: - new_e.append(n) - if len(new_e) > 0: - new_e_list.append(new_e) - - X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list) - - IDX_e = list(range(0, len(X.e[0]))) - - sizes = [ - len(X.e[0][i]) for i in IDX_e - ] # hyperedges with size <m are removed - idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m] - now_e_list = X.e[0] - new_e_list = [] - for i in range(len(now_e_list)): - if i not in idx_e_remove: - new_e_list.append(now_e_list[i]) - - X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list) - - node_set = set() - for sublist in X.e[0]: - for element in sublist: - node_set.add(element) - IDX = list(node_set) - - while len(idx_n_remove) > 0 or len(idx_e_remove) > 0: - d_tot_m = np.zeros(len(IDX)) - - for i in range(len(IDX)): - d_tot_m[i] = X.degree_node[IDX[i]] - - idx_n_remove = list( - compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m)) - ) # nodes with degree<k are removed - # X.remove_nodes_from(idx_n_remove) - now_e_list = X.e[0] - new_e_list = [] - for e in now_e_list: - new_e = [] - for n in e: - if n not in idx_n_remove: - new_e.append(n) - if len(new_e) > 0: - new_e_list.append(new_e) - X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list) - - IDX_e = list(range(len(X.e[0]))) - sizes = [ - len(X.e[0][i]) for i in IDX_e - ] # hyperedges with size <m are removed - - idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m] - now_e_list = X.e[0] - new_e_list = [] - for i in range(len(now_e_list)): - if i not in idx_e_remove: - new_e_list.append(now_e_list[i]) - X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list) - - node_set = set() - for sublist in X.e[0]: - for element in sublist: - node_set.add(element) - IDX = list(node_set) - - shell_kk = list(sorted(set(prev_shell) - set(IDX))) - for j in shell_kk: - # if j not in idx_n_remove: - # continue - k_shell_dict[j][x] = kk - k_step - - node_set = set() - for sublist in X.e[0]: - for element in sublist: - node_set.add(element) - IDX = list(node_set) - - D[y] = len(node_set) - if y > 0: - if D[y] == 0 and D[y - 1] != 0: - # maximum connectivity at order m - k_max[x] = kk - k_step - # stop the decomposition when the (k,m)-core is empty - if D[y] == 0: - break - - # size-independent hypercoreness - R_dict = {} - for y in k_shell_dict: - R_dict[y] = sum(np.array(k_shell_dict[y]) / np.array(k_max)) - - return R_dict
- - -
[docs]def frequency_based_hypercoreness(h): - r"""The frequency-based hypercoreness of nodes in hypergraph. - - Parameters - ---------- - h : easygraph.Hypergraph - - - Returns - ------- - dict : Centrality, where keys are node IDs and values are lists of centralities. - - References - ---------- - Mancastroppa, M., Iacopini, I., Petri, G. et al. Hyper-cores promote localization and efficient seeding in higher-order processes. Nat Commun 14, 6223 (2023). https://doi.org/10.1038/s41467-023-41887-2 - - """ - e_list = h.e[0] - initial_node_num = h.num_v - data = [e_list[i] for i in range(len(e_list)) if len(e_list[i]) > 1] - - data.sort(key=len) - L = len(data) - size_max = len(data[L - 1]) - - size = list([len(data[j]) for j in range(L)]) - - X = eg.Hypergraph(num_v=initial_node_num, e_list=data) - IDX = list(range(0, X.num_v)) - - M = range(2, size_max + 1) - k_step = 1 - K = range(1, 1200, k_step) - k_shell_dict = {} - idx_orig = IDX - - IDX_size = range(len(size)) - k_max = np.zeros(len(M)) - - for j in idx_orig: - k_shell_dict[j] = np.zeros(len(M)) - - for x in range(len(M)): - m = M[x] - - D = np.zeros(len(K)) - # consider only hyperedges of size >=m - idx_size = list( - compress(IDX_size, np.greater_equal(size, m * np.ones(len(size)))) - ) - int_sel = list([data[i] for i in idx_size]) - # build hypergraph with only interactions of size >=m - X = eg.Hypergraph(num_v=initial_node_num, e_list=int_sel) - node_set = set() - for sublist in int_sel: - for element in sublist: - node_set.add(element) - IDX = list(node_set) - - for y in range(len(K)): - kk = K[y] - - d_tot_m = np.zeros(len(IDX)) - prev_shell = IDX - - for i in range(len(IDX)): - d_tot_m[i] = X.degree_node[IDX[i]] - - idx_n_remove = list( - compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m)) - ) # nodes with degree<k are removed - now_e_list = X.e[0] - new_e_list = [] - for e in now_e_list: - new_e = [] - for n in e: - if n not in idx_n_remove: - new_e.append(n) - if len(new_e) > 0: - new_e_list.append(new_e) - - X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list) - - IDX_e = list(range(0, len(X.e[0]))) - - # hyperedges with size <m are removed - sizes = [len(X.e[0][i]) for i in IDX_e] - idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m] - now_e_list = X.e[0] - new_e_list = [] - for i in range(len(now_e_list)): - if i not in idx_e_remove: - new_e_list.append(now_e_list[i]) - - X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list) - - node_set = set() - for sublist in X.e[0]: - for element in sublist: - node_set.add(element) - IDX = list(node_set) - - while len(idx_n_remove) > 0 or len(idx_e_remove) > 0: - d_tot_m = np.zeros(len(IDX)) - - for i in range(len(IDX)): - d_tot_m[i] = X.degree_node[IDX[i]] - # nodes with degree<k are removed - idx_n_remove = list( - compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m)) - ) - now_e_list = X.e[0] - new_e_list = [] - for e in now_e_list: - new_e = [] - for n in e: - if n not in idx_n_remove: - new_e.append(n) - if len(new_e) > 0: - new_e_list.append(new_e) - X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list) - - IDX_e = list(range(len(X.e[0]))) - # hyperedges with size <m are removed - sizes = [len(X.e[0][i]) for i in IDX_e] - - idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m] - now_e_list = X.e[0] - new_e_list = [] - for i in range(len(now_e_list)): - if i not in idx_e_remove: - new_e_list.append(now_e_list[i]) - X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list) - - node_set = set() - for sublist in X.e[0]: - for element in sublist: - node_set.add(element) - IDX = list(node_set) - - shell_kk = list(sorted(set(prev_shell) - set(IDX))) - for j in shell_kk: - k_shell_dict[j][x] = kk - k_step - - node_set = set() - for sublist in X.e[0]: - for element in sublist: - node_set.add(element) - IDX = list(node_set) - - D[y] = len(node_set) - if y > 0: - if D[y] == 0 and D[y - 1] != 0: - k_max[x] = kk - k_step # maximum connectivity at order m - if D[y] == 0: - break # stop the decomposition when the (k,m)-core is empty - - # Psi(m) distribution of hyperedges size - Psi = [] - for m in range(2, size_max + 1): - Psi.append(size.count(m) / len(size)) - # frequency-based hypercoreness - R_w_dict = {} - for y in k_shell_dict: - R_w_dict[y] = sum(np.array(Psi) * np.array(k_shell_dict[y]) / np.array(k_max)) - return R_w_dict
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/centrality/s_centrality.html b/docs/_modules/easygraph/functions/hypergraph/centrality/s_centrality.html deleted file mode 100644 index b6b50851..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/centrality/s_centrality.html +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - easygraph.functions.hypergraph.centrality.s_centrality — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.centrality.s_centrality

-import easygraph as eg
-
-
-__all__ = ["s_betweenness", "s_closeness", "s_eccentricity"]
-
-
-
[docs]def s_betweenness(H, s=1, weight=False, n_workers=None): - """Computes the betweenness centrality for each edge in the hypergraph. - - Computes the betweenness centrality for each edge in the hypergraph. - - Parameters - ---------- - H : eg.Hypergraph. - The hypergraph to compute - - s : int, optional. - - Returns - ---------- - dict - The keys are the edges and the values are the betweenness centrality. - The betweenness centrality for each edge in the hypergraph. - - - """ - - linegraph = H.get_linegraph(s=s, weight=weight) - results = eg.betweenness_centrality(linegraph, n_workers=n_workers) - return results
- - -
[docs]def s_closeness(H, s=1, weight=False, n_workers=None): - """ - Compute the closeness centrality for each edge in the hypergraph. - - Parameters - ---------- - H : eg.Hypergraph. - s : int, optional - - Returns - ------- - dict. The closeness centrality for each edge in the hypergraph. The keys are the edges and the values are the closeness centrality. - """ - linegraph = H.get_linegraph(s=s, weight=weight) - results = eg.closeness_centrality(linegraph, n_workers=n_workers) - return results
- - -
[docs]def s_eccentricity(H, s=1, edges=True, source=None): - r""" - The length of the longest shortest path from a vertex $u$ to every other vertex in - the s-linegraph. - $V$ = set of vertices in the s-linegraph - $d$ = shortest path distance - - .. math:: - - \text{s-ecc}(u) = \text{max}\{d(u,v): v \in V\} - - Parameters - ---------- - H : eg.Hypergraph - - s : int, optional - - edges : bool, optional - Indicates if method should compute edge linegraph (default) or node linegraph. - - source : str, optional - Identifier of node or edge of interest for computing centrality - - Returns - ------- - dict or float - returns the s-eccentricity value of the edges(nodes). - If source=None a dictionary of values for each s-edge in H is returned. - If source then a single value is returned. - If the s-linegraph is disconnected, np.inf is returned. - - """ - - g = H.get_linegraph(s=s, edges=edges) - result = eg.eccentricity(g) - if source: - return result[source] - else: - return result
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/centrality/vector_centrality.html b/docs/_modules/easygraph/functions/hypergraph/centrality/vector_centrality.html deleted file mode 100644 index 9f815da7..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/centrality/vector_centrality.html +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - easygraph.functions.hypergraph.centrality.vector_centrality — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.centrality.vector_centrality

-import easygraph as eg
-import numpy as np
-
-from easygraph.exception import EasyGraphError
-
-
-__all__ = ["vector_centrality"]
-
-
-
[docs]def vector_centrality(H): - """The vector centrality of nodes in the line graph of the hypergraph. - - Parameters - ---------- - H : eg.Hypergraph - - - Returns - ------- - dict - Centrality, where keys are node IDs and values are lists of centralities. - - References - ---------- - "Vector centrality in hypergraphs", K. Kovalenko, M. Romance, E. Vasilyeva, - D. Aleja, R. Criado, D. Musatov, A.M. Raigorodskii, J. Flores, I. Samoylenko, - K. Alfaro-Bittner, M. Perc, S. Boccaletti, - https://doi.org/10.1016/j.chaos.2022.112397 - - """ - - # If the hypergraph is empty, then return an empty dictionary - if H.num_v == 0: - return dict() - - LG = H.get_linegraph() - if not eg.is_connected(LG): - raise EasyGraphError("This method is not defined for disconnected hypergraphs.") - LGcent = eg.eigenvector_centrality(LG) - - vc = {node: [] for node in range(0, H.num_v)} - - edge_label_dict = {tuple(edge): index for index, edge in enumerate(H.e[0])} - - hyperedge_dims = {tuple(edge): len(edge) for edge in H.e[0]} - - D = max([len(e) for e in H.e[0]]) - - for k in range(2, D + 1): - c_i = np.zeros(H.num_v) - - for edge, _ in list(filter(lambda x: x[1] == k, hyperedge_dims.items())): - for node in edge: - try: - c_i[node] += LGcent[edge_label_dict[edge]] - except IndexError: - raise Exception( - "Nodes must be written with the Pythonic indexing (0,1,2...)" - ) - - c_i *= 1 / k - - for node in range(H.num_v): - vc[node].append(c_i[node]) - - return vc
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/hypergraph_clustering.html b/docs/_modules/easygraph/functions/hypergraph/hypergraph_clustering.html deleted file mode 100644 index 77c951a8..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/hypergraph_clustering.html +++ /dev/null @@ -1,408 +0,0 @@ - - - - - - easygraph.functions.hypergraph.hypergraph_clustering — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.hypergraph_clustering

-"""Algorithms for computing nodal clustering coefficients."""
-
-import numpy as np
-
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = [
-    "hypergraph_clustering_coefficient",
-    "hypergraph_local_clustering_coefficient",
-    "hypergraph_two_node_clustering_coefficient",
-]
-
-
-
[docs]def hypergraph_clustering_coefficient(H): - r"""Return the clustering coefficients for - each node in a Hypergraph. - - This clustering coefficient is defined as the - clustering coefficient of the unweighted pairwise - projection of the hypergraph, i.e., - :math:`c = A^3_{i,i}/\binom{k}{2},` - where :math:`A` is the adjacency matrix of the network - and :math:`k` is the pairwise degree of :math:`i`. - - Parameters - ---------- - H : Hypergraph - Hypergraph - - Returns - ------- - dict - nodes are keys, clustering coefficients are values. - - Notes - ----- - The clustering coefficient is undefined when the number of - neighbors is 0 or 1, but we set the clustering coefficient - to 0 in these cases. For more discussion, see - https://arxiv.org/abs/0802.2512 - - See Also - -------- - local_clustering_coefficient - two_node_clustering_coefficient - - References - ---------- - "Clustering Coefficients in Protein Interaction Hypernetworks" - by Suzanne Gallagher and Debra Goldberg. - DOI: 10.1145/2506583.2506635 - - Example - ------- - >>> import easygraph as eg - >>> H = eg.random_hypergraph(3, [1, 1]) - >>> cc = eg.clustering_coefficient(H) - >>> cc - {0: 1.0, 1: 1.0, 2: 1.0} - """ - adj = H.adjacency_matrix() - k = np.array(adj.sum(axis=1)) - l = [] - for i in k: - l.append(i[0]) - k = np.array(l) - denom = k * (k - 1) / 2 - mat = adj.dot(adj).dot(adj) - with np.errstate(divide="ignore", invalid="ignore"): - result = np.nan_to_num(0.5 * mat.diagonal() / denom) - r = {} - for i in range(0, len(H.v)): - r[i] = result[i] - return r
- - -
[docs]def hypergraph_local_clustering_coefficient(H): - """Compute the local clustering coefficient. - - This clustering coefficient is based on the - overlap of the edges connected to a given node, - normalized by the size of the node's neighborhood. - - Parameters - ---------- - H : Hypergraph - Hypergraph - - Returns - ------- - dict - keys are node IDs and values are the - clustering coefficients. - - Notes - ----- - The clustering coefficient is undefined when the number of - neighbors is 0 or 1, but we set the clustering coefficient - to 0 in these cases. For more discussion, see - https://arxiv.org/abs/0802.2512 - - See Also - -------- - clustering_coefficient - two_node_clustering_coefficient - - References - ---------- - "Properties of metabolic graphs: biological organization or representation - artifacts?" by Wanding Zhou and Luay Nakhleh. - https://doi.org/10.1186/1471-2105-12-132 - - "Hypergraphs for predicting essential genes using multiprotein complex data" - by Florian Klimm, Charlotte M. Deane, and Gesine Reinert. - https://doi.org/10.1093/comnet/cnaa028 - - Example - ------- - >>> import easygraph as eg - >>> H = eg.random_hypergraph(3, [1, 1]) - >>> cc = eg.hypergraph_local_clustering_coefficient(H) - >>> cc - {0: 1.0, 1: 1.0, 2: 1.0} - - """ - result = {} - # 节点属于哪些边 - memberships = [] - for n in H.v: - tmp = set() - for index, e in enumerate(H.e[0]): - if n in e: - tmp.add(index) - memberships.append(tmp) - - # 每条边包含哪些节点 - members = H.e[0] - for n in H.v: - ev = memberships[n] - dv = len(ev) - if dv <= 1: - result[n] = 0 - else: - total_eo = 0 - # go over all pairs of edges pairwise - for e1 in range(dv): - edge1 = members[e1] - for e2 in range(e1): - edge2 = members[e2] - # set differences for the hyperedges - D1 = set(edge1) - set(edge2) - D2 = set(edge2) - set(edge1) - # if edges are the same by definition the extra overlap is zero - if len(D1.union(D2)) == 0: - eo = 0 - else: - # otherwise we have to look at their neighbors - # the neighbors of D1 and D2, respectively. - neighD1 = {i for d in D1 for i in H.neighbor_of_node(d)} - neighD2 = {i for d in D2 for i in H.neighbor_of_node(d)} - # compute extra overlap [len() is used for cardinality of edges] - eo = ( - len(neighD1.intersection(D2)) - + len(neighD2.intersection(D1)) - ) / len( - D1.union(D2) - ) # add it up - # add it up - total_eo = total_eo + eo - - # include normalization by degree k*(k-1)/2 - result[n] = 2 * total_eo / (dv * (dv - 1)) - return result
- - -
[docs]def hypergraph_two_node_clustering_coefficient(H, kind="union"): - """Return the clustering coefficients for - each node in a Hypergraph. - - This definition averages over all of the - two-node clustering coefficients involving the node. - - Parameters - ---------- - H : Hypergraph - Hypergraph - kind : string, optional - The type of two node clustering coefficient. Options - are "union", "max", and "min". By default, "union". - - Returns - ------- - dict - nodes are keys, clustering coefficients are values. - - Notes - ----- - The clustering coefficient is undefined when the number of - neighbors is 0 or 1, but we set the clustering coefficient - to 0 in these cases. For more discussion, see - https://arxiv.org/abs/0802.2512 - - See Also - -------- - clustering_coefficient - local_clustering_coefficient - - References - ---------- - "Clustering Coefficients in Protein Interaction Hypernetworks" - by Suzanne Gallagher and Debra Goldberg. - DOI: 10.1145/2506583.2506635 - - Example - ------- - >>> import easygraph as eg - >>> H = eg.random_hypergraph(3, [1, 1]) - >>> cc = eg.two_node_clustering_coefficient(H, kind="union") - >>> cc - {0: 0.5, 1: 0.5, 2: 0.5} - """ - result = {} - memberships = {} - for n in H.v: - tmp = set() - for index, e in enumerate(H.e[0]): - if n in e: - tmp.add(index) - memberships[n] = tmp - - for n in H.v: - neighbors = H.neighbor_of_node(n) - result[n] = 0.0 - for v in neighbors: - result[n] += _uv_cc(n, v, memberships, kind=kind) / len(neighbors) - return result
- - -def _uv_cc(u, v, memberships, kind="union"): - """Helper function to compute the two-node - clustering coefficient. - - Parameters - ---------- - u : hashable - First node - v : hashable - Second node - memberships : dict - node IDs are keys, edge IDs to which they belong - are values. - kind : str, optional - Type of clustering coefficient to compute, by default "union". - Options: - - - "union" - - "max" - - "min" - - Returns - ------- - float - The clustering coefficient - - Raises - ------ - EasyGraphError - If an invalid clustering coefficient kind - is specified. - - References - ---------- - "Clustering Coefficients in Protein Interaction Hypernetworks" - by Suzanne Gallagher and Debra Goldberg. - DOI: 10.1145/2506583.2506635 - """ - m_u = memberships[u] - m_v = memberships[v] - - num = len(m_u.intersection(m_v)) - - if kind == "union": - denom = len(m_u.union(m_v)) - elif kind == "min": - denom = min(len(m_u), len(m_v)) - elif kind == "max": - denom = max(len(m_u), len(m_v)) - else: - raise EasyGraphError("Invalid kind of clustering.") - - if denom == 0: - return np.nan - - return num / denom -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/hypergraph_operation.html b/docs/_modules/easygraph/functions/hypergraph/hypergraph_operation.html deleted file mode 100644 index 3107cf7a..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/hypergraph_operation.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - easygraph.functions.hypergraph.hypergraph_operation — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.hypergraph_operation

-from easygraph.exception import EasyGraphError
-
-
-__all__ = [
-    "hypergraph_density",
-]
-
-
-
[docs]def hypergraph_density(hg, ignore_singletons=False): - r"""Hypergraph density. - - The density of a hypergraph is the number of existing edges divided by the number of - possible edges. - - Let `H` have :math:`n` nodes and :math:`m` hyperedges. Then, - - * `density(H) =` :math:`\frac{m}{2^n - 1}`, - * `density(H, ignore_singletons=True) =` :math:`\frac{m}{2^n - 1 - n}`. - - Here, :math:`2^n` is the total possible number of hyperedges on `H`, from which we - subtract :math:`1` because the empty hyperedge is not considered. We subtract an - additional :math:`n` when singletons are not considered. - - Now assume `H` has :math:`a` edges with order :math:`1` and :math:`b` edges with - order :math:`2`. Then, - - * `density(H, order=1) =` :math:`\frac{a}{{n \choose 2}}`, - * `density(H, order=2) =` :math:`\frac{b}{{n \choose 3}}`, - * `density(H, max_order=1) =` :math:`\frac{a}{{n \choose 1} + {n \choose 2}}`, - * `density(H, max_order=1, ignore_singletons=True) =` :math:`\frac{a}{{n \choose 2}}`, - * `density(H, max_order=2) =` :math:`\frac{m}{{n \choose 1} + {n \choose 2} + {n \choose 3}}`, - * `density(H, max_order=2, ignore_singletons=True) =` :math:`\frac{m}{{n \choose 2} + {n \choose 3}}`, - - Parameters - --------- - order : int, optional - If not None, only count edges of the specified order. - By default, None. - - max_order : int, optional - If not None, only count edges of order up to this value, inclusive. - By default, None. - - ignore_singletons : bool, optional - Whether to consider singleton edges. Ignored if `order` is not None and - different from :math:`0`. By default, False. - - See Also - -------- - :func:`incidence_density` - - Notes - ----- - If both `order` and `max_order` are not None, `max_order` is ignored. - - """ - n = hg.num_v - numer = len(hg.e[0]) - if n < 1: - raise EasyGraphError("Density not defined for empty hypergraph") - if numer < 1: - return 0.0 - - denom = 2**n - 1 - if ignore_singletons: - denom -= n - try: - return numer / float(denom) - except ZeroDivisionError: - return 0.0
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/null_model/hypergraph_classic.html b/docs/_modules/easygraph/functions/hypergraph/null_model/hypergraph_classic.html deleted file mode 100644 index 42cd5969..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/null_model/hypergraph_classic.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - easygraph.functions.hypergraph.null_model.hypergraph_classic — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.null_model.hypergraph_classic

-import itertools
-
-import easygraph as eg
-
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = ["empty_hypergraph", "complete_hypergraph"]
-
-
-
[docs]def empty_hypergraph(N=1): - """ - - Parameters - ---------- - N number of node in Hypergraph, default 1 - - Returns - ------- - A eg.Hypergraph with n_num node, without any hyperedge. - - """ - return eg.Hypergraph(N)
- - -
[docs]def complete_hypergraph(n, include_singleton=False): - if n == 0: - raise EasyGraphError("The number of nodes in a Hypergraph can not be zero") - # init - # print("easygraph:",eg) - hypergraph = eg.Hypergraph(n) - total_hyperedegs = [] - if n > 1: - start = 1 if include_singleton else 2 - for size in range(start, n + 1): - hyperedges = itertools.combinations(list(range(n)), size) - total_hyperedegs.extend(list(hyperedges)) - hypergraph.add_hyperedges(total_hyperedegs) - return hypergraph
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/null_model/lattice.html b/docs/_modules/easygraph/functions/hypergraph/null_model/lattice.html deleted file mode 100644 index 4c4ffd6a..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/null_model/lattice.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - easygraph.functions.hypergraph.null_model.lattice — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.null_model.lattice

-"""Generators for some lattice hypergraphs.
-
-All the functions in this module return a Hypergraph class (i.e. a simple, undirected
-hypergraph).
-
-"""
-
-from warnings import warn
-
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = [
-    "ring_lattice",
-]
-
-
-
[docs]def ring_lattice(n, d, k, l): - """A ring lattice hypergraph. - - A d-uniform hypergraph on n nodes where each node is part of k edges and the - overlap between consecutive edges is d-l. - - Parameters - ---------- - n : int - Number of nodes - d : int - Edge size - k : int - Number of edges of which a node is a part. Should be a multiple of 2. - l : int - Overlap between edges - - Returns - ------- - Hypergraph - The generated hypergraph - - Raises - ------ - EasyGraphError - If k is negative. - - Notes - ----- - ring_lattice(n, 2, k, 0) is a ring lattice graph where each node has k//2 edges on - either side. - - """ - from easygraph.classes.hypergraph import Hypergraph - - if k < 0: - raise EasyGraphError("Invalid k value!") - - if k < 2: - warn("This creates a completely disconnected hypergraph!") - - if k % 2 != 0: - warn("k is not divisible by 2") - - edges = [ - [node] + [(start + l + i) % n for i in range(d - 1)] - for node in range(n) - for start in range(node + 1, node + k // 2 + 1) - ] - H = Hypergraph(num_v=n) - H.add_hyperedges(edges) - return H
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/null_model/random.html b/docs/_modules/easygraph/functions/hypergraph/null_model/random.html deleted file mode 100644 index a1bf17c9..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/null_model/random.html +++ /dev/null @@ -1,559 +0,0 @@ - - - - - - easygraph.functions.hypergraph.null_model.random — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.null_model.random

-import math
-import random
-import warnings
-
-from collections import defaultdict
-from itertools import combinations
-
-import easygraph as eg
-import numpy as np
-
-# from easygraph.classes.hypergraph import Hypergraph
-from easygraph.utils.exception import EasyGraphError
-from scipy.special import comb
-
-from .lattice import *
-
-
-__all__ = [
-    "random_hypergraph",
-    "chung_lu_hypergraph",
-    "dcsbm_hypergraph",
-    "watts_strogatz_hypergraph",
-    "uniform_hypergraph_Gnp",
-]
-
-
-def uniform_hypergraph_Gnp_parallel(edges, prob):
-    remain_edges = [e for e in edges if random.random() < prob]
-    return remain_edges
-
-
-def split_edges(edges, worker):
-    import math
-
-    edges_size = len(edges)
-    group_size = math.ceil(edges_size / worker)
-    group_lst = []
-    for i in range(0, edges_size, group_size):
-        group_lst.append(edges[i : i + group_size])
-
-    return group_lst
-
-
-
[docs]def uniform_hypergraph_Gnp(k: int, num_v: int, prob: float, n_workers=None): - r"""Return a random ``k``-uniform hypergraph with ``num_v`` vertices and probability ``prob`` of choosing a hyperedge. - - Args: - ``num_v`` (``int``): The Number of vertices. - ``k`` (``int``): The Number of vertices in each hyperedge. - ``prob`` (``float``): Probability of choosing a hyperedge. - - Examples: - >>> import easygraph as eg - >>> hg = eg.random.uniform_hypergraph_Gnp(3, 5, 0.5) - >>> hg.e - ([(0, 1, 3), (0, 1, 4), (0, 2, 4), (1, 3, 4), (2, 3, 4)], [1.0, 1.0, 1.0, 1.0, 1.0]) - """ - # similar to BinomialRandomUniform in sagemath, https://doc.sagemath.org/html/en/reference/graphs/sage/graphs/hypergraph_generators.html - - assert num_v > 1, "num_v must be greater than 1" - assert k > 1, "k must be greater than 1" - assert 0 <= prob <= 1, "prob must be between 0 and 1" - import random - - if n_workers is not None: - # use the parallel version for large graph - - from functools import partial - from multiprocessing import Pool - - edges = combinations(range(num_v), k) - edges_parallel = split_edges(edges=list(edges), worker=n_workers) - local_function = partial(uniform_hypergraph_Gnp_parallel, prob=prob) - - res_edges = [] - - with Pool(n_workers) as p: - ret = p.imap(local_function, edges_parallel) - for res in ret: - res_edges.extend(res) - res_hypergraph = eg.Hypergraph(num_v=num_v, e_list=res_edges) - return res_hypergraph - - else: - edges = combinations(range(num_v), k) - edges = [e for e in edges if random.random() < prob] - return eg.Hypergraph(num_v=num_v, e_list=edges)
- - -
[docs]def dcsbm_hypergraph(k1, k2, g1, g2, omega, seed=None): - """A function to generate a Degree-Corrected Stochastic Block Model - (DCSBM) hypergraph. - - Parameters - ---------- - k1 : dict - This is a dictionary where the keys are node ids - and the values are node degrees. - k2 : dict - This is a dictionary where the keys are edge ids - and the values are edge sizes. - g1 : dict - This a dictionary where the keys are node ids - and the values are the group ids to which the node belongs. - The keys must match the keys of k1. - g2 : dict - This a dictionary where the keys are edge ids - and the values are the group ids to which the edge belongs. - The keys must match the keys of k2. - omega : 2D numpy array - This is a matrix with entries which specify the number of edges - between a given node community and edge community. - The number of rows must match the number of node communities - and the number of columns must match the number of edge - communities. - seed : int or None (default) - Seed for the random number generator. - - Returns - ------- - Hypergraph - - Warns - ----- - warnings.warn - If the sums of the edge sizes and node degrees are not equal, the - algorithm still runs, but raises a warning. - Also if the sum of the omega matrix does not match the sum of degrees, - a warning is raised. - - Notes - ----- - The sums of k1 and k2 should be the same. If they are not the same, this function - returns a warning but still runs. The sum of k1 (and k2) and omega should be the - same. If they are not the same, this function returns a warning but still runs and - the number of entries in the incidence matrix is determined by the omega matrix. - - References - ---------- - Implemented by Mirah Shi in HyperNetX and described for bipartite networks by - Larremore et al. in https://doi.org/10.1103/PhysRevE.90.012805 - - Examples - -------- - >>> import easygraph as eg; import random; import numpy as np - >>> n = 50 - >>> k1 = {i : random.randint(1, n) for i in range(n)} - >>> k2 = {i : sorted(k1.values())[i] for i in range(n)} - >>> g1 = {i : random.choice([0, 1]) for i in range(n)} - >>> g2 = {i : random.choice([0, 1]) for i in range(n)} - >>> omega = np.array([[n//2, 10], [10, n//2]]) - >>> H = eg.dcsbm_hypergraph(k1, k2, g1, g2, omega) - - """ - if seed is not None: - random.seed(seed) - - # sort dictionary by degree in decreasing order - node_labels = [n for n, _ in sorted(k1.items(), key=lambda d: d[1], reverse=True)] - edge_labels = [m for m, _ in sorted(k2.items(), key=lambda d: d[1], reverse=True)] - - # Verify that the sum of node and edge degrees and the sum of node degrees and the - # sum of community connection matrix differ by less than a single edge. - if abs(sum(k1.values()) - sum(k2.values())) > 1: - warnings.warn( - "The sum of the degree sequence does not match the sum of the size sequence" - ) - - if abs(sum(k1.values()) - np.sum(omega)) > 1: - warnings.warn( - "The sum of the degree sequence does not " - "match the entries in the omega matrix" - ) - - # get indices for each community - community1_nodes = defaultdict(list) - for label in node_labels: - group = g1[label] - community1_nodes[group].append(label) - - community2_nodes = defaultdict(list) - for label in edge_labels: - group = g2[label] - community2_nodes[group].append(label) - - H = eg.Hypergraph(num_v=len(node_labels)) - - kappa1 = defaultdict(lambda: 0) - kappa2 = defaultdict(lambda: 0) - for id, g in g1.items(): - kappa1[g] += k1[id] - for id, g in g2.items(): - kappa2[g] += k2[id] - - tmp_hyperedges = [] - for group1 in community1_nodes.keys(): - for group2 in community2_nodes.keys(): - # for each constant probability patch - try: - group_constant = omega[group1, group2] / ( - kappa1[group1] * kappa2[group2] - ) - except ZeroDivisionError: - group_constant = 0 - - for u in community1_nodes[group1]: - j = 0 - v = community2_nodes[group2][j] # start from beginning every time - # max probability - p = min(k1[u] * k2[v] * group_constant, 1) - while j < len(community2_nodes[group2]): - if p != 1: - r = random.random() - try: - j = j + math.floor(math.log(r) / math.log(1 - p)) - except ZeroDivisionError: - j = np.inf - if j < len(community2_nodes[group2]): - v = community2_nodes[group2][j] - q = min((k1[u] * k2[v]) * group_constant, 1) - r = random.random() - if r < q / p: - # no duplicates - if v < len(tmp_hyperedges): - if u not in tmp_hyperedges[v]: - tmp_hyperedges[v].append(u) - else: - tmp_hyperedges.append([u]) - - p = q - j = j + 1 - - H.add_hyperedges(tmp_hyperedges) - return H
- - -
[docs]def watts_strogatz_hypergraph(n, d, k, l, p, seed=None): - """ - - Parameters - ---------- - n : int - The number of nodes - d : int - Edge size - k: int - Number of edges of which a node is a part. Should be a multiple of 2. - l: int - Overlap between edges - p : float - The probability of rewiring each edge - seed - - Returns - ------- - - """ - if seed is not None: - np.random.seed(seed) - H = ring_lattice(n, d, k, l) - to_remove = [] - to_add = [] - H_edges = H.e[0] - for e in H_edges: - if np.random.random() < p: - to_remove.append(e) - node = min(e) - neighbors = np.random.choice(H.v, size=d - 1) - to_add.append(np.append(neighbors, node)) - - for e in to_remove: - if e in H_edges: - H_edges.remove(e) - - for e in to_add: - H_edges.append(e) - - H = eg.Hypergraph(num_v=n, e_list=H_edges) - # H.remove_hyperedges(to_remove) - # print("watts_strogatz:",H.e) - # H.add_hyperedges(to_add) - - return H
- - -
[docs]def chung_lu_hypergraph(k1, k2, seed=None): - """A function to generate a Chung-Lu hypergraph - - Parameters - ---------- - k1 : dict - Dict where the keys are node ids - and the values are node degrees. - k2 : dict - dict where the keys are edge ids - and the values are edge sizes. - seed : integer or None (default) - The seed for the random number generator. - - Returns - ------- - Hypergraph object - The generated hypergraph - - Warns - ----- - warnings.warn - If the sums of the edge sizes and node degrees are not equal, the - algorithm still runs, but raises a warning. - - Notes - ----- - The sums of k1 and k2 should be the same. If they are not the same, - this function returns a warning but still runs. - - References - ---------- - Implemented by Mirah Shi in HyperNetX and described for - bipartite networks by Aksoy et al. in https://doi.org/10.1093/comnet/cnx001 - - Example - ------- - >>> import easygraph as eg - >>> import random - >>> n = 100 - >>> k1 = {i : random.randint(1, 100) for i in range(n)} - >>> k2 = {i : sorted(k1.values())[i] for i in range(n)} - >>> H = eg.chung_lu_hypergraph(k1, k2) - - """ - if seed is not None: - random.seed(seed) - - # sort dictionary by degree in decreasing order - node_labels = [n for n, _ in sorted(k1.items(), key=lambda d: d[1], reverse=True)] - edge_labels = [m for m, _ in sorted(k2.items(), key=lambda d: d[1], reverse=True)] - - m = len(k2) - - if sum(k1.values()) != sum(k2.values()): - warnings.warn( - "The sum of the degree sequence does not match the sum of the size sequence" - ) - - S = sum(k1.values()) - - H = eg.Hypergraph(len(node_labels)) - - tmp_hyperedges = [] - for u in node_labels: - j = 0 - v = edge_labels[j] # start from beginning every time - p = min((k1[u] * k2[v]) / S, 1) - - while j < m: - if p != 1: - r = random.random() - try: - j = j + math.floor(math.log(r) / math.log(1 - p)) - except ZeroDivisionError: - j = np.inf - - if j < m: - v = edge_labels[j] - q = min((k1[u] * k2[v]) / S, 1) - r = random.random() - if r < q / p: - # no duplicates - if v < len(tmp_hyperedges): - tmp_hyperedges[v].append(u) - else: - tmp_hyperedges.append([u]) - p = q - j = j + 1 - - H.add_hyperedges(tmp_hyperedges) - return H
- - -
[docs]def random_hypergraph(N, ps, order=None, seed=None): - """Generates a random hypergraph - - Generate N nodes, and connect any d+1 nodes - by a hyperedge with probability ps[d-1]. - - Parameters - ---------- - N : int - Number of nodes - ps : list of float - List of probabilities (between 0 and 1) to create a - hyperedge at each order d between any d+1 nodes. For example, - ps[0] is the wiring probability of any edge (2 nodes), ps[1] - of any triangles (3 nodes). - order: int of None (default) - If None, ignore. If int, generates a uniform hypergraph with edges - of order `order` (ps must have only one element). - seed : integer or None (default) - Seed for the random number generator. - - Returns - ------- - Hypergraph object - The generated hypergraph - - References - ---------- - Described as 'random hypergraph' by M. Dewar et al. in https://arxiv.org/abs/1703.07686 - - Example - ------- - >>> import easygraph as eg - >>> H = eg.random_hypergraph(50, [0.1, 0.01]) - - """ - if seed is not None: - np.random.seed(seed) - - if order is not None: - if len(ps) != 1: - raise EasyGraphError("ps must contain a single element if order is an int") - - if (np.any(np.array(ps) < 0)) or (np.any(np.array(ps) > 1)): - raise EasyGraphError("All elements of ps must be between 0 and 1 included.") - - nodes = range(N) - hyperedges = [] - - for i, p in enumerate(ps): - if order is not None: - d = order - else: - d = i + 1 # order, ps[0] is prob of edges (d=1) - - potential_edges = combinations(nodes, d + 1) - n_comb = comb(N, d + 1, exact=True) - mask = np.random.random(size=n_comb) <= p # True if edge to keep - - edges_to_add = [e for e, val in zip(potential_edges, mask) if val] - - hyperedges += edges_to_add - - H = eg.Hypergraph(num_v=N) - H.add_hyperedges(hyperedges) - - return H
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/null_model/simple.html b/docs/_modules/easygraph/functions/hypergraph/null_model/simple.html deleted file mode 100644 index 3292a272..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/null_model/simple.html +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - easygraph.functions.hypergraph.null_model.simple — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.null_model.simple

-from itertools import combinations
-
-import easygraph as eg
-
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = [
-    "star_clique",
-]
-
-
-
[docs]def star_clique(n_star, n_clique, d_max): - """Generate a star-clique structure - - That is a star network and a clique network, - connected by one pairwise edge connecting the centre of the star to the clique. - network, the each clique is promoted to a hyperedge - up to order d_max. - - Parameters - ---------- - n_star : int - Number of legs of the star - n_clique : int - Number of nodes in the clique - d_max : int - Maximum order up to which to promote - cliques to hyperedges - - Returns - ------- - H : Hypergraph - - Examples - -------- - >>> import easygraph as eg - >>> H = eg.star_clique(6, 7, 2) - - Notes - ----- - The total number of nodes is n_star + n_clique. - - """ - - if n_star <= 0: - raise ValueError("n_star must be an integer > 0.") - if n_clique <= 0: - raise ValueError("n_clique must be an integer > 0.") - if d_max < 0: - raise ValueError("d_max must be an integer >= 0.") - elif d_max > n_clique - 1: - raise ValueError("d_max must be <= n_clique - 1.") - - nodes_star = range(n_star) - nodes_clique = range(n_star, n_star + n_clique) - nodes = list(nodes_star) + list(nodes_clique) - - H = eg.Hypergraph(num_v=len(nodes)) - - # add star edges (center of the star is 0-th node) - H.add_hyperedges([[nodes_star[0], nodes_star[i]] for i in range(1, n_star)]) - - # connect clique and star by adding last star leg - H.add_hyperedges([nodes_star[0], nodes_clique[0]]) - - # add clique hyperedges up to order d_max - - H.add_hyperedges( - [ - e - for d in range(1, d_max + 1) - for e in list(combinations(nodes_clique, d + 1)) - ] - ) - - return H
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/hypergraph/null_model/uniform.html b/docs/_modules/easygraph/functions/hypergraph/null_model/uniform.html deleted file mode 100644 index 93efa483..00000000 --- a/docs/_modules/easygraph/functions/hypergraph/null_model/uniform.html +++ /dev/null @@ -1,568 +0,0 @@ - - - - - - easygraph.functions.hypergraph.null_model.uniform — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.hypergraph.null_model.uniform

-"""Generate random uniform hypergraphs."""
-import itertools
-import operator
-import random
-import warnings
-
-from functools import reduce
-
-import easygraph as eg
-import numpy as np
-
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = [
-    "uniform_hypergraph_configuration_model",
-    "uniform_HSBM",
-    "uniform_HPPM",
-    "uniform_erdos_renyi_hypergraph",
-    "uniform_hypergraph_Gnm",
-]
-
-
-def split_num_e(num_e, worker):
-    import math
-
-    res = []
-    group_size = num_e // worker
-    for i in range(worker):
-        res.append(group_size)
-    return res
-
-
-def uniform_hypergraph_Gnm_parallel(num_e, num_v, k):
-    random.seed()
-    edges = set()
-    while len(edges) < num_e:
-        e = random.sample(range(num_v), k)
-        e = tuple(sorted(e))
-        if e not in edges:
-            edges.add(e)
-    return list(edges)
-
-
-
[docs]def uniform_hypergraph_Gnm(k: int, num_v: int, num_e: int, n_workers=None): - r"""Return a random ``k``-uniform hypergraph with ``num_v`` vertices and ``num_e`` hyperedges. - - Args: - ``k`` (``int``): The Number of vertices in each hyperedge. - ``num_v`` (``int``): The Number of vertices. - ``num_e`` (``int``): The Number of hyperedges. - - Examples: - >>> import easygraph as eg - >>> hg = eg.uniform_hypergraph_Gnm(3, 5, 4) - >>> hg.e - ([(0, 1, 2), (0, 1, 3), (0, 3, 4), (2, 3, 4)], [1.0, 1.0, 1.0, 1.0]) - """ - # similar to UniformRandomUniform in sagemath, https://doc.sagemath.org/html/en/reference/graphs/sage/graphs/hypergraph_generators.html - - assert k > 1, "k must be greater than 1" # TODO ? - assert num_v > 1, "num_v must be greater than 1" - assert num_e > 0, "num_e must be greater than 0" - - if n_workers is not None: - # use the parallel version for large graph - edges = set() - from functools import partial - from multiprocessing import Pool - - # res_edges = set() - edges_parallel = split_num_e(num_e=num_e, worker=n_workers) - local_function = partial(uniform_hypergraph_Gnm_parallel, num_v=num_v, k=k) - - res_edges = set() - import time - - with Pool(n_workers) as p: - ret = p.imap(local_function, edges_parallel) - for res in ret: - for r in res: - res_edges.add(r) - - while len(res_edges) < num_e: - e = random.sample(range(num_v), k) - e = tuple(sorted(e)) - if e not in res_edges: - res_edges.add(e) - - res_hypergraph = eg.Hypergraph(num_v=num_v, e_list=list(res_edges)) - return res_hypergraph - - else: - edges = set() - while len(edges) < num_e: - e = random.sample(range(num_v), k) - e = tuple(sorted(e)) - if e not in edges: - edges.add(e) - - return eg.Hypergraph(num_v, list(edges))
- - -
[docs]def uniform_hypergraph_configuration_model(k, m, seed=None): - """ - A function to generate an m-uniform configuration model - - Parameters - ---------- - k : dictionary - This is a dictionary where the keys are node ids - and the values are node degrees. - m : int - specifies the hyperedge size - seed : integer or None (default) - The seed for the random number generator - - Returns - ------- - Hypergraph object - The generated hypergraph - - Warns - ----- - warnings.warn - If the sums of the degrees are not divisible by m, the - algorithm still runs, but raises a warning and adds an - additional connection to random nodes to satisfy this - condition. - - Notes - ----- - This algorithm normally creates multi-edges and loopy hyperedges. - We remove the loopy hyperedges. - - References - ---------- - "The effect of heterogeneity on hypergraph contagion models" - by Nicholas W. Landry and Juan G. Restrepo - https://doi.org/10.1063/5.0020034 - - - Example - ------- - >>> import easygraph as eg - >>> import random - >>> n = 1000 - >>> m = 3 - >>> k = {1: 1, 2: 2, 3: 3, 4: 3} - >>> H = eg.uniform_hypergraph_configuration_model(k, m) - - """ - if seed is not None: - random.seed(seed) - - # Making sure we have the right number of stubs - remainder = sum(k.values()) % m - if remainder != 0: - warnings.warn( - "This degree sequence is not realizable. " - "Increasing the degree of random nodes so that it is." - ) - random_ids = random.sample(list(k.keys()), int(round(m - remainder))) - for id in random_ids: - k[id] = k[id] + 1 - - stubs = [] - # Creating the list to index through - for id in k: - stubs.extend([id] * int(k[id])) - - H = eg.Hypergraph(num_v=len(k)) - - while len(stubs) != 0: - u = random.sample(range(len(stubs)), m) - edge = set() - for index in u: - edge.add(stubs[index]) - if len(edge) == m: - H.add_hyperedges(list(edge)) - - for index in sorted(u, reverse=True): - del stubs[index] - - return H
- - -
[docs]def uniform_HSBM(n, m, p, sizes, seed=None): - """Create a uniform hypergraph stochastic block model (HSBM). - - Parameters - ---------- - n : int - The number of nodes - m : int - The hyperedge size - p : m-dimensional numpy array - tensor of probabilities between communities - sizes : list or 1D numpy array - The sizes of the community blocks in order - seed : integer or None (default) - The seed for the random number generator - - Returns - ------- - Hypergraph - The constructed SBM hypergraph - - Raises - ------ - EasyGraphError - - If the length of sizes and p do not match. - - If p is not a tensor with every dimension equal - - If p is not m-dimensional - - If the entries of p are not in the range [0, 1] - - If the sum of the vector of sizes does not equal the number of nodes. - Exception - If there is an integer overflow error - - See Also - -------- - uniform_HPPM - - References - ---------- - Nicholas W. Landry and Juan G. Restrepo. - "Polarization in hypergraphs with community structure." - Preprint, 2023. https://doi.org/10.48550/arXiv.2302.13967 - """ - # Check if dimensions match - if len(sizes) != np.size(p, axis=0): - raise EasyGraphError("'sizes' and 'p' do not match.") - if len(np.shape(p)) != m: - raise EasyGraphError("The dimension of p does not match m") - # Check that p has the same length over every dimension. - if len(set(np.shape(p))) != 1: - raise EasyGraphError("'p' must be a square tensor.") - if np.max(p) > 1 or np.min(p) < 0: - raise EasyGraphError("Entries of 'p' not in [0,1].") - if np.sum(sizes) != n: - raise EasyGraphError("Sum of sizes does not match n") - - if seed is not None: - np.random.seed(seed) - - node_labels = range(n) - H = eg.Hypergraph(num_v=n) - - block_range = range(len(sizes)) - # Split node labels in a partition (list of sets). - size_cumsum = [sum(sizes[0:x]) for x in range(0, len(sizes) + 1)] - partition = [ - list(node_labels[size_cumsum[x] : size_cumsum[x + 1]]) - for x in range(0, len(size_cumsum) - 1) - ] - - for block in itertools.product(block_range, repeat=m): - if p[block] == 1: # Test edges cases p_ij = 0 or 1 - edges = itertools.product((partition[i] for i in block_range)) - for e in edges: - H.add_hyperedges(list(e)) - elif p[block] > 0: - partition_sizes = [len(partition[i]) for i in block] - max_index = reduce(operator.mul, partition_sizes, 1) - if max_index < 0: - raise Exception("Index overflow error!") - index = np.random.geometric(p[block]) - 1 - - while index < max_index: - indices = _index_to_edge_partition(index, partition_sizes, m) - e = {partition[block[i]][indices[i]] for i in range(m)} - if len(e) == m: - H.add_hyperedges(list(e)) - index += np.random.geometric(p[block]) - return H
- - -
[docs]def uniform_HPPM(n, m, rho, k, epsilon, seed=None): - """Construct the m-uniform hypergraph planted partition model (m-HPPM) - - Parameters - ---------- - n : int > 0 - Number of nodes - m : int > 0 - Hyperedge size - rho : float between 0 and 1 - The fraction of nodes in community 1 - k : float > 0 - Mean degree - epsilon : float > 0 - Imbalance parameter - seed : integer or None (default) - The seed for the random number generator - - Returns - ------- - Hypergraph - The constructed m-HPPM hypergraph. - - Raises - ------ - EasyGraphError - - If rho is not between 0 and 1 - - If the mean degree is negative. - - If epsilon is not between 0 and 1 - - See Also - -------- - uniform_HSBM - - References - ---------- - Nicholas W. Landry and Juan G. Restrepo. - "Polarization in hypergraphs with community structure." - Preprint, 2023. https://doi.org/10.48550/arXiv.2302.13967 - """ - - if rho < 0 or rho > 1: - raise EasyGraphError("The value of rho must be between 0 and 1") - if k < 0: - raise EasyGraphError("The mean degree must be non-negative") - if epsilon < 0 or epsilon > 1: - raise EasyGraphError("epsilon must be between 0 and 1") - - sizes = [int(rho * n), n - int(rho * n)] - - p = k / (m * n ** (m - 1)) - # ratio of inter- to intra-community edges - q = rho**m + (1 - rho) ** m - r = 1 / q - 1 - p_in = (1 + r * epsilon) * p - p_out = (1 - epsilon) * p - - p = p_out * np.ones([2] * m) - p[tuple([0] * m)] = p_in - p[tuple([1] * m)] = p_in - - return uniform_HSBM(n, m, p, sizes, seed=seed)
- - -
[docs]def uniform_erdos_renyi_hypergraph(n, m, p, p_type="degree", seed=None): - """Generate an m-uniform Erdős–Rényi hypergraph - - This creates a hypergraph with `n` nodes where - hyperedges of size `m` are created at random to - obtain a mean degree of `k`. - - Parameters - ---------- - n : int > 0 - Number of nodes - m : int > 0 - Hyperedge size - p : float or int > 0 - Mean expected degree if p_type="degree" and - probability of an m-hyperedge if p_type="prob" - p_type : str - "degree" or "prob", by default "degree" - seed : integer or None (default) - The seed for the random number generator - - Returns - ------- - Hypergraph - The Erdos Renyi hypergraph - - - See Also - -------- - random_hypergraph - """ - if seed is not None: - np.random.seed(seed) - - H = eg.Hypergraph(num_v=n) - - if p_type == "degree": - q = p / (m * n ** (m - 1)) # wiring probability - elif p_type == "prob": - q = p - else: - raise EasyGraphError("Invalid p_type!") - - if q > 1 or q < 0: - raise EasyGraphError("Probability not in [0,1].") - - index = np.random.geometric(q) - 1 # -1 b/c zero indexing - max_index = n**m - while index < max_index: - e = set(_index_to_edge(index, n, m)) - if len(e) == m: - H.add_hyperedges(list(e)) - index += np.random.geometric(q) - return H
- - -def _index_to_edge(index, n, m): - """Generate a hyperedge given an index in the list of possible edges. - - Parameters - ---------- - index : int > 0 - The index of the hyperedge in the list of all possible hyperedges. - n : int > 0 - The number of nodes - m : int > 0 - The hyperedge size. - - Returns - ------- - list - The reconstructed hyperedge - - See Also - -------- - _index_to_edge_partition - - References - ---------- - https://stackoverflow.com/questions/53834707/element-at-index-in-itertools-product - """ - return [(index // (n**r) % n) for r in range(m - 1, -1, -1)] - - -def _index_to_edge_partition(index, partition_sizes, m): - """Generate a hyperedge given an index in the list of possible edges - and a partition of community labels. - - Parameters - ---------- - index : int > 0 - The index of the hyperedge in the list of all possible hyperedges. - n : int > 0 - The number of nodes - m : int > 0 - The hyperedge size. - - Returns - ------- - list - The reconstructed hyperedge - - See Also - -------- - _index_to_edge - - """ - try: - return [ - int(index // np.prod(partition_sizes[r + 1 :]) % partition_sizes[r]) - for r in range(m) - ] - except KeyError: - raise Exception("Invalid parameters") -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/isolate.html b/docs/_modules/easygraph/functions/isolate.html deleted file mode 100644 index be58028e..00000000 --- a/docs/_modules/easygraph/functions/isolate.html +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - easygraph.functions.isolate — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • »
  • -
  • Module code »
  • -
  • easygraph.functions.isolate
  • -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.isolate

-"""
-Functions for identifying isolate (degree zero) nodes.
-"""
-
-__all__ = ["is_isolate", "isolates", "number_of_isolates"]
-
-
-
[docs]def is_isolate(G, n): - """Determines whether a node is an isolate. - - An *isolate* is a node with no neighbors (that is, with degree - zero). For directed graphs, this means no in-neighbors and no - out-neighbors. - - Parameters - ---------- - G : EasyGraph graph - - n : node - A node in `G`. - - Returns - ------- - is_isolate : bool - True if and only if `n` has no neighbors. - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_edge(1, 2) - >>> G.add_node(3) - >>> eg.is_isolate(G, 2) - False - >>> eg.is_isolate(G, 3) - True - """ - return G.degree()[n] == 0
- - -
[docs]def isolates(G): - """Iterator over isolates in the graph. - - An *isolate* is a node with no neighbors (that is, with degree - zero). For directed graphs, this means no in-neighbors and no - out-neighbors. - - Parameters - ---------- - G : EasyGraph graph - - Returns - ------- - iterator - An iterator over the isolates of `G`. - - Examples - -------- - To get a list of all isolates of a graph, use the :class:`list` - constructor:: - - >>> G = eg.Graph() - >>> G.add_edge(1, 2) - >>> G.add_node(3) - >>> list(eg.isolates(G)) - [3] - - To remove all isolates in the graph, first create a list of the - isolates, then use :meth:`Graph.remove_nodes_from`:: - - >>> G.remove_nodes_from(list(eg.isolates(G))) - >>> list(G) - [1, 2] - - For digraphs, isolates have zero in-degree and zero out_degre:: - - >>> G = eg.DiGraph([(0, 1), (1, 2)]) - >>> G.add_node(3) - >>> list(eg.isolates(G)) - [3] - - """ - return (n for n, d in G.degree().items() if d == 0)
- - -
[docs]def number_of_isolates(G): - """Returns the number of isolates in the graph. - - An *isolate* is a node with no neighbors (that is, with degree - zero). For directed graphs, this means no in-neighbors and no - out-neighbors. - - Parameters - ---------- - G : EasyGraph graph - - Returns - ------- - int - The number of degree zero nodes in the graph `G`. - - """ - # TODO This can be parallelized. - return sum(1 for v in isolates(G))
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/path/bridges.html b/docs/_modules/easygraph/functions/path/bridges.html deleted file mode 100644 index cea5519a..00000000 --- a/docs/_modules/easygraph/functions/path/bridges.html +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - easygraph.functions.path.bridges — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.path.bridges

-from itertools import chain
-
-import easygraph as eg
-
-from easygraph.utils.decorators import *
-
-
-__all__ = ["bridges", "has_bridges"]
-
-
-
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -def bridges(G, root=None): - """Generate all bridges in a graph. - - A *bridge* in a graph is an edge whose removal causes the number of - connected components of the graph to increase. Equivalently, a bridge is an - edge that does not belong to any cycle. - - Parameters - ---------- - G : undirected graph - - root : node (optional) - A node in the graph `G`. If specified, only the bridges in the - connected component containing this node will be returned. - - Yields - ------ - e : edge - An edge in the graph whose removal disconnects the graph (or - causes the number of connected components to increase). - - Raises - ------ - NodeNotFound - If `root` is not in the graph `G`. - - Examples - -------- - - >>> list(eg.bridges(G)) - [(9, 10)] - - Notes - ----- - This is an implementation of the algorithm described in _[1]. An edge is a - bridge if and only if it is not contained in any chain. Chains are found - using the :func:`chain_decomposition` function. - - Ignoring polylogarithmic factors, the worst-case time complexity is the - same as the :func:`chain_decomposition` function, - $O(m + n)$, where $n$ is the number of nodes in the graph and $m$ is - the number of edges. - - References - ---------- - .. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29#Bridge-Finding_with_Chain_Decompositions - """ - chains = chain_decomposition(G, root=root) - chain_edges = set(chain.from_iterable(chains)) - for u, v, t in G.edges: - if (u, v) not in chain_edges and (v, u) not in chain_edges: - yield u, v
- - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -def has_bridges(G, root=None): - """Decide whether a graph has any bridges. - - A *bridge* in a graph is an edge whose removal causes the number of - connected components of the graph to increase. - - Parameters - ---------- - G : undirected graph - - root : node (optional) - A node in the graph `G`. If specified, only the bridges in the - connected component containing this node will be considered. - - Returns - ------- - bool - Whether the graph (or the connected component containing `root`) - has any bridges. - - Raises - ------ - NodeNotFound - If `root` is not in the graph `G`. - - Examples - -------- - - >>> eg.has_bridges(G) - True - - Notes - ----- - This implementation uses the :func:`easygraph.bridges` function, so - it shares its worst-case time complexity, $O(m + n)$, ignoring - polylogarithmic factors, where $n$ is the number of nodes in the - graph and $m$ is the number of edges. - - """ - try: - next(bridges(G)) - except StopIteration: - return False - else: - return True
- - -def chain_decomposition(G, root=None): - def _dfs_cycle_forest(G, root=None): - H = eg.DiGraph() - nodes = [] - for u, v, d in dfs_labeled_edges(G, source=root): - if d == "forward": - # `dfs_labeled_edges()` yields (root, root, 'forward') - # if it is beginning the search on a new connected - # component. - if u == v: - H.add_node(v, parent=None) - nodes.append(v) - else: - H.add_node(v, parent=u) - H.add_edge(v, u, nontree=False) - nodes.append(v) - # `dfs_labeled_edges` considers nontree edges in both - # orientations, so we need to not add the edge if it its - # other orientation has been added. - elif d == "nontree" and v not in H[u]: - H.add_edge(v, u, nontree=True) - else: - # Do nothing on 'reverse' edges; we only care about - # forward and nontree edges. - pass - return H, nodes - - def _build_chain(G, u, v, visited): - while v not in visited: - yield u, v - visited.add(v) - u, v = v, G.nodes[v]["parent"] - yield u, v - - H, nodes = _dfs_cycle_forest(G, root) - - visited = set() - for u in nodes: - visited.add(u) - # For each nontree edge going out of node u... - edges = [] - for w, v, d in H.edges: - if w == u and d["nontree"] == True: - edges.append((w, v)) - # edges = ((u, v) for u, v, d in H.out_edges(u, data="nontree") if d) - for u, v in edges: - # Create the cycle or cycle prefix starting with the - # nontree edge. - chain = list(_build_chain(H, u, v, visited)) - yield chain - - -def dfs_labeled_edges(G, source=None, depth_limit=None): - if source is None: - # edges for all components - nodes = G - else: - # edges for components with source - nodes = [source] - visited = set() - if depth_limit is None: - depth_limit = len(G) - for start in nodes: - if start in visited: - continue - yield start, start, "forward" - visited.add(start) - stack = [(start, depth_limit, iter(G[start]))] - while stack: - parent, depth_now, children = stack[-1] - try: - child = next(children) - if child in visited: - yield parent, child, "nontree" - else: - yield parent, child, "forward" - visited.add(child) - if depth_now > 1: - stack.append((child, depth_now - 1, iter(G[child]))) - except StopIteration: - stack.pop() - if stack: - yield stack[-1][0], parent, "reverse" - yield start, start, "reverse" -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/path/diameter.html b/docs/_modules/easygraph/functions/path/diameter.html deleted file mode 100644 index bd8d8bce..00000000 --- a/docs/_modules/easygraph/functions/path/diameter.html +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - easygraph.functions.path.diameter — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.path.diameter

-import easygraph as eg
-import easygraph.functions.path
-
-
-__all__ = ["diameter", "eccentricity"]
-
-
-
[docs]def eccentricity(G, v=None, sp=None): - """Returns the eccentricity of nodes in G. - - The eccentricity of a node v is the maximum distance from v to - all other nodes in G. - - Parameters - ---------- - G : EasyGraph graph - A graph - - v : node, optional - Return value of specified node - - sp : dict of dicts, optional - All pairs shortest path lengths as a dictionary of dictionaries - - Returns - ------- - ecc : dictionary - A dictionary of eccentricity values keyed by node. - - Examples - -------- - >>> G = eg.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) - >>> dict(eg.eccentricity(G)) - {1: 2, 2: 3, 3: 2, 4: 2, 5: 3} - - >>> dict(eg.eccentricity(G, v=[1, 5])) # This returns the eccentrity of node 1 & 5 - {1: 2, 5: 3} - - """ - # if v is None: # none, use entire graph - # nodes=G.nodes() - # elif v in G: # is v a single node - # nodes=[v] - # else: # assume v is a container of nodes - # nodes=v - order = G.order() - - e = {} - for n in G.nbunch_iter(v): - if sp is None: - length = eg.single_source_dijkstra(G, n) - L = len(length) - else: - try: - length = sp[n] - L = len(length) - except TypeError as err: - raise eg.EasyGraphError('Format of "sp" is invalid.') from err - if L != order: - if G.is_directed(): - msg = ( - "Found infinite path length because the digraph is not" - " strongly connected" - ) - else: - msg = "Found infinite path length because the graph is not connected" - raise eg.EasyGraphError(msg) - - e[n] = max(length.values()) - - if v in G: - return e[v] # return single value - else: - return e
- - -
[docs]def diameter(G, e=None): - """Returns the diameter of the graph G. - - The diameter is the maximum eccentricity. - - Parameters - ---------- - G : EasyGraph graph - A graph - - e : eccentricity dictionary, optional - A precomputed dictionary of eccentricities. - - Returns - ------- - d : integer - Diameter of graph - - Examples - -------- - >>> G = eg.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) - >>> eg.diameter(G) - 3 - - See Also - -------- - eccentricity - """ - - if e is None: - e = eccentricity(G) - return max(e.values())
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/path/mst.html b/docs/_modules/easygraph/functions/path/mst.html deleted file mode 100644 index 9c34597d..00000000 --- a/docs/_modules/easygraph/functions/path/mst.html +++ /dev/null @@ -1,798 +0,0 @@ - - - - - - easygraph.functions.path.mst — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.functions.path.mst

-from heapq import heappop
-from heapq import heappush
-from itertools import count
-from math import isnan
-from operator import itemgetter
-
-from easygraph.utils.decorators import *
-
-
-__all__ = [
-    "minimum_spanning_edges",
-    "maximum_spanning_edges",
-    "minimum_spanning_tree",
-    "maximum_spanning_tree",
-]
-
-
-def boruvka_mst_edges(G, minimum=True, weight="weight", data=True, ignore_nan=False):
-    """Iterate over edges of a Borůvka's algorithm min/max spanning tree.
-
-    Parameters
-    ----------
-    G : EasyGraph Graph
-        The edges of `G` must have distinct weights,
-        otherwise the edges may not form a tree.
-
-    minimum : bool (default: True)
-        Find the minimum (True) or maximum (False) spanning tree.
-
-    weight : string (default: 'weight')
-        The name of the edge attribute holding the edge weights.
-
-    data : bool (default: True)
-        Flag for whether to yield edge attribute dicts.
-        If True, yield edges `(u, v, d)`, where `d` is the attribute dict.
-        If False, yield edges `(u, v)`.
-
-    ignore_nan : bool (default: False)
-        If a NaN is found as an edge weight normally an exception is raised.
-        If `ignore_nan is True` then that edge is ignored instead.
-
-    """
-    # Initialize a forest, assuming initially that it is the discrete
-    # partition of the nodes of the graph.
-    forest = UnionFind(G)
-
-    def best_edge(component):
-        """Returns the optimum (minimum or maximum) edge on the edge
-        boundary of the given set of nodes.
-
-        A return value of ``None`` indicates an empty boundary.
-
-        """
-        sign = 1 if minimum else -1
-        minwt = float("inf")
-        boundary = None
-        for e in edge_boundary(G, component, data=True):
-            wt = e[-1].get(weight, 1) * sign
-            if isnan(wt):
-                if ignore_nan:
-                    continue
-                msg = f"NaN found as an edge weight. Edge {e}"
-                raise ValueError(msg)
-            if wt < minwt:
-                minwt = wt
-                boundary = e
-        return boundary
-
-    # Determine the optimum edge in the edge boundary of each component
-    # in the forest.
-    best_edges = (best_edge(component) for component in forest.to_sets())
-    best_edges = [edge for edge in best_edges if edge is not None]
-    # If each entry was ``None``, that means the graph was disconnected,
-    # so we are done generating the forest.
-    while best_edges:
-        # Determine the optimum edge in the edge boundary of each
-        # component in the forest.
-        #
-        # This must be a sequence, not an iterator. In this list, the
-        # same edge may appear twice, in different orientations (but
-        # that's okay, since a union operation will be called on the
-        # endpoints the first time it is seen, but not the second time).
-        #
-        # Any ``None`` indicates that the edge boundary for that
-        # component was empty, so that part of the forest has been
-        # completed.
-        #
-        # TODO This can be parallelized, both in the outer loop over
-        # each component in the forest and in the computation of the
-        # minimum. (Same goes for the identical lines outside the loop.)
-        best_edges = (best_edge(component) for component in forest.to_sets())
-        best_edges = [edge for edge in best_edges if edge is not None]
-        # Join trees in the forest using the best edges, and yield that
-        # edge, since it is part of the spanning tree.
-        #
-        # TODO This loop can be parallelized, to an extent (the union
-        # operation must be atomic).
-        for u, v, d in best_edges:
-            if forest[u] != forest[v]:
-                if data:
-                    yield u, v, d
-                else:
-                    yield u, v
-                forest.union(u, v)
-
-
-@hybrid("cpp_kruskal_mst_edges")
-def kruskal_mst_edges(G, minimum=True, weight="weight", data=True, ignore_nan=False):
-    """Iterate over edges of a Kruskal's algorithm min/max spanning tree.
-
-    Parameters
-    ----------
-    G : EasyGraph Graph
-        The graph holding the tree of interest.
-
-    minimum : bool (default: True)
-        Find the minimum (True) or maximum (False) spanning tree.
-
-    weight : string (default: 'weight')
-        The name of the edge attribute holding the edge weights.
-
-    data : bool (default: True)
-        Flag for whether to yield edge attribute dicts.
-        If True, yield edges `(u, v, d)`, where `d` is the attribute dict.
-        If False, yield edges `(u, v)`.
-
-    ignore_nan : bool (default: False)
-        If a NaN is found as an edge weight normally an exception is raised.
-        If `ignore_nan is True` then that edge is ignored instead.
-
-    """
-    subtrees = UnionFind()
-    edges = []
-    for u, v, t in G.edges:
-        edges.append((u, v, t))
-
-    def filter_nan_edges(edges=edges, weight=weight):
-        sign = 1 if minimum else -1
-        for u, v, d in edges:
-            wt = d.get(weight, 1) * sign
-            if isnan(wt):
-                if ignore_nan:
-                    continue
-                msg = f"NaN found as an edge weight. Edge {(u, v, d)}"
-                raise ValueError(msg)
-            yield wt, u, v, d
-
-    edges = sorted(filter_nan_edges(), key=itemgetter(0))
-    for wt, u, v, d in edges:
-        if subtrees[u] != subtrees[v]:
-            if data:
-                yield (u, v, d)
-            else:
-                yield (u, v)
-            subtrees.union(u, v)
-
-
-@hybrid("cpp_prim_mst_edges")
-def prim_mst_edges(G, minimum=True, weight="weight", data=True, ignore_nan=False):
-    """Iterate over edges of Prim's algorithm min/max spanning tree.
-
-    Parameters
-    ----------
-    G : EasyGraph Graph
-        The graph holding the tree of interest.
-
-    minimum : bool (default: True)
-        Find the minimum (True) or maximum (False) spanning tree.
-
-    weight : string (default: 'weight')
-        The name of the edge attribute holding the edge weights.
-
-    data : bool (default: True)
-        Flag for whether to yield edge attribute dicts.
-        If True, yield edges `(u, v, d)`, where `d` is the attribute dict.
-        If False, yield edges `(u, v)`.
-
-    ignore_nan : bool (default: False)
-        If a NaN is found as an edge weight normally an exception is raised.
-        If `ignore_nan is True` then that edge is ignored instead.
-
-    """
-    push = heappush
-    pop = heappop
-
-    nodes = set(G)
-    c = count()
-
-    sign = 1 if minimum else -1
-
-    while nodes:
-        u = nodes.pop()
-        frontier = []
-        visited = {u}
-        for v, d in G.adj[u].items():
-            wt = d.get(weight, 1) * sign
-            if isnan(wt):
-                if ignore_nan:
-                    continue
-                msg = f"NaN found as an edge weight. Edge {(u, v, d)}"
-                raise ValueError(msg)
-            push(frontier, (wt, next(c), u, v, d))
-        while frontier:
-            W, _, u, v, d = pop(frontier)
-            if v in visited or v not in nodes:
-                continue
-            if data:
-                yield u, v, d
-            else:
-                yield u, v
-            # update frontier
-            visited.add(v)
-            nodes.discard(v)
-            for w, d2 in G.adj[v].items():
-                if w in visited:
-                    continue
-                new_weight = d2.get(weight, 1) * sign
-                push(frontier, (new_weight, next(c), v, w, d2))
-
-
-ALGORITHMS = {
-    "boruvka": boruvka_mst_edges,
-    "borůvka": boruvka_mst_edges,
-    "kruskal": kruskal_mst_edges,
-    "prim": prim_mst_edges,
-}
-
-
-
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -def minimum_spanning_edges( - G, algorithm="kruskal", weight="weight", data=True, ignore_nan=False -): - """Generate edges in a minimum spanning forest of an undirected - weighted graph. - - A minimum spanning tree is a subgraph of the graph (a tree) - with the minimum sum of edge weights. A spanning forest is a - union of the spanning trees for each connected component of the graph. - - Parameters - ---------- - G : undirected Graph - An undirected graph. If `G` is connected, then the algorithm finds a - spanning tree. Otherwise, a spanning forest is found. - - algorithm : string - The algorithm to use when finding a minimum spanning tree. Valid - choices are 'kruskal', 'prim', or 'boruvka'. The default is 'kruskal'. - - weight : string - Edge data key to use for weight (default 'weight'). - - data : bool, optional - If True yield the edge data along with the edge. - - ignore_nan : bool (default: False) - If a NaN is found as an edge weight normally an exception is raised. - If `ignore_nan is True` then that edge is ignored instead. - - Returns - ------- - edges : iterator - An iterator over edges in a maximum spanning tree of `G`. - Edges connecting nodes `u` and `v` are represented as tuples: - `(u, v, k, d)` or `(u, v, k)` or `(u, v, d)` or `(u, v)` - - Examples - -------- - >>> from easygraph.functions.basic import mst - - Find minimum spanning edges by Kruskal's algorithm - - >>> G.add_edge(0, 3, weight=2) - >>> mst = mst.minimum_spanning_edges(G, algorithm="kruskal", data=False) - >>> edgelist = list(mst) - >>> sorted(sorted(e) for e in edgelist) - [[0, 1], [1, 2], [2, 3]] - - Find minimum spanning edges by Prim's algorithm - - >>> G.add_edge(0, 3, weight=2) - >>> mst = mst.minimum_spanning_edges(G, algorithm="prim", data=False) - >>> edgelist = list(mst) - >>> sorted(sorted(e) for e in edgelist) - [[0, 1], [1, 2], [2, 3]] - - Notes - ----- - For Borůvka's algorithm, each edge must have a weight attribute, and - each edge weight must be distinct. - - For the other algorithms, if the graph edges do not have a weight - attribute a default weight of 1 will be used. - - Modified code from David Eppstein, April 2006 - http://www.ics.uci.edu/~eppstein/PADS/ - - """ - try: - algo = ALGORITHMS[algorithm] - except KeyError as e: - msg = f"{algorithm} is not a valid choice for an algorithm." - raise ValueError(msg) from e - - return algo(G, minimum=True, weight=weight, data=data, ignore_nan=ignore_nan)
- - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -def maximum_spanning_edges( - G, algorithm="kruskal", weight="weight", data=True, ignore_nan=False -): - """Generate edges in a maximum spanning forest of an undirected - weighted graph. - - A maximum spanning tree is a subgraph of the graph (a tree) - with the maximum possible sum of edge weights. A spanning forest is a - union of the spanning trees for each connected component of the graph. - - Parameters - ---------- - G : undirected Graph - An undirected graph. If `G` is connected, then the algorithm finds a - spanning tree. Otherwise, a spanning forest is found. - - algorithm : string - The algorithm to use when finding a maximum spanning tree. Valid - choices are 'kruskal', 'prim', or 'boruvka'. The default is 'kruskal'. - - weight : string - Edge data key to use for weight (default 'weight'). - - data : bool, optional - If True yield the edge data along with the edge. - - ignore_nan : bool (default: False) - If a NaN is found as an edge weight normally an exception is raised. - If `ignore_nan is True` then that edge is ignored instead. - - Returns - ------- - edges : iterator - An iterator over edges in a maximum spanning tree of `G`. - Edges connecting nodes `u` and `v` are represented as tuples: - `(u, v, k, d)` or `(u, v, k)` or `(u, v, d)` or `(u, v)` - - Examples - -------- - >>> from easygraph.functions.path import mst - - Find maximum spanning edges by Kruskal's algorithm - - >>> G.add_edge(0, 3, weight=2) - >>> mst = mst.maximum_spanning_edges(G, algorithm="kruskal", data=False) - >>> edgelist = list(mst) - >>> sorted(sorted(e) for e in edgelist) - [[0, 1], [0, 3], [1, 2]] - - Find maximum spanning edges by Prim's algorithm - - >>> G.add_edge(0, 3, weight=2) # assign weight 2 to edge 0-3 - >>> mst = mst.maximum_spanning_edges(G, algorithm="prim", data=False) - >>> edgelist = list(mst) - >>> sorted(sorted(e) for e in edgelist) - [[0, 1], [0, 3], [2, 3]] - - Notes - ----- - For Borůvka's algorithm, each edge must have a weight attribute, and - each edge weight must be distinct. - - For the other algorithms, if the graph edges do not have a weight - attribute a default weight of 1 will be used. - - Modified code from David Eppstein, April 2006 - http://www.ics.uci.edu/~eppstein/PADS/ - """ - try: - algo = ALGORITHMS[algorithm] - except KeyError as e: - msg = f"{algorithm} is not a valid choice for an algorithm." - raise ValueError(msg) from e - - return algo(G, minimum=False, weight=weight, data=data, ignore_nan=ignore_nan)
- - -
[docs]@not_implemented_for("multigraph") -def minimum_spanning_tree(G, weight="weight", algorithm="kruskal", ignore_nan=False): - """Returns a minimum spanning tree or forest on an undirected graph `G`. - - Parameters - ---------- - G : undirected graph - An undirected graph. If `G` is connected, then the algorithm finds a - spanning tree. Otherwise, a spanning forest is found. - - weight : str - Data key to use for edge weights. - - algorithm : string - The algorithm to use when finding a minimum spanning tree. Valid - choices are 'kruskal', 'prim', or 'boruvka'. The default is - 'kruskal'. - - ignore_nan : bool (default: False) - If a NaN is found as an edge weight normally an exception is raised. - If `ignore_nan is True` then that edge is ignored instead. - - Returns - ------- - G : EasyGraph Graph - A minimum spanning tree or forest. - - Examples - -------- - >>> G.add_edge(0, 3, weight=2) - >>> T = eg.minimum_spanning_tree(G) - >>> sorted(T.edges(data=True)) - [(0, 1, {}), (1, 2, {}), (2, 3, {})] - - - Notes - ----- - For Borůvka's algorithm, each edge must have a weight attribute, and - each edge weight must be distinct. - - For the other algorithms, if the graph edges do not have a weight - attribute a default weight of 1 will be used. - - Isolated nodes with self-loops are in the tree as edgeless isolated nodes. - - """ - edges = list( - minimum_spanning_edges(G, algorithm, weight, data=True, ignore_nan=ignore_nan) - ) - T = G.__class__() # Same graph class as G - for i in G.nodes: - T.add_node(i) - for i in edges: - (u, v, t) = i - T.add_edge(u, v, **t) - return T
- - -
[docs]@not_implemented_for("multigraph") -def maximum_spanning_tree(G, weight="weight", algorithm="kruskal", ignore_nan=False): - """Returns a maximum spanning tree or forest on an undirected graph `G`. - - Parameters - ---------- - G : undirected graph - An undirected graph. If `G` is connected, then the algorithm finds a - spanning tree. Otherwise, a spanning forest is found. - - weight : str - Data key to use for edge weights. - - algorithm : string - The algorithm to use when finding a maximum spanning tree. Valid - choices are 'kruskal', 'prim', or 'boruvka'. The default is - 'kruskal'. - - ignore_nan : bool (default: False) - If a NaN is found as an edge weight normally an exception is raised. - If `ignore_nan is True` then that edge is ignored instead. - - - Returns - ------- - G : EasyGraph Graph - A maximum spanning tree or forest. - - - Examples - -------- - >>> G.add_edge(0, 3, weight=2) - >>> T = eg.maximum_spanning_tree(G) - >>> sorted(T.edges(data=True)) - [(0, 1, {}), (0, 3, {'weight': 2}), (1, 2, {})] - - - Notes - ----- - For Borůvka's algorithm, each edge must have a weight attribute, and - each edge weight must be distinct. - - For the other algorithms, if the graph edges do not have a weight - attribute a default weight of 1 will be used. - - There may be more than one tree with the same minimum or maximum weight. - See :mod:`easygraph.tree.recognition` for more detailed definitions. - - Isolated nodes with self-loops are in the tree as edgeless isolated nodes. - - """ - edges = list( - maximum_spanning_edges(G, algorithm, weight, data=True, ignore_nan=ignore_nan) - ) - T = G.__class__() # Same graph class as G - for i in G.nodes: - T.add_node(i) - for i in edges: - (u, v, t) = i - T.add_edge(u, v, **t) - return T
- - -def edge_boundary(G, nbunch1, nbunch2=None, data=False, default=None): - """Returns the edge boundary of `nbunch1`. - - The *edge boundary* of a set *S* with respect to a set *T* is the - set of edges (*u*, *v*) such that *u* is in *S* and *v* is in *T*. - If *T* is not specified, it is assumed to be the set of all nodes - not in *S*. - - Parameters - ---------- - G : EasyGraph graph - - nbunch1 : iterable - Iterable of nodes in the graph representing the set of nodes - whose edge boundary will be returned. (This is the set *S* from - the definition above.) - - nbunch2 : iterable - Iterable of nodes representing the target (or "exterior") set of - nodes. (This is the set *T* from the definition above.) If not - specified, this is assumed to be the set of all nodes in `G` - not in `nbunch1`. - - data : bool or object - This parameter has the same meaning as in - :meth:`MultiGraph.edges`. - - default : object - This parameter has the same meaning as in - :meth:`MultiGraph.edges`. - - Returns - ------- - iterator - An iterator over the edges in the boundary of `nbunch1` with - respect to `nbunch2`. If `keys`, `data`, or `default` - are specified and `G` is a multigraph, then edges are returned - with keys and/or data, as in :meth:`MultiGraph.edges`. - - Notes - ----- - Any element of `nbunch` that is not in the graph `G` will be - ignored. - - `nbunch1` and `nbunch2` are usually meant to be disjoint, but in - the interest of speed and generality, that is not required here. - - """ - nset1 = {v for v in G if v in nbunch1} - # Here we create an iterator over edges incident to nodes in the set - # `nset1`. The `Graph.edges()` method does not provide a guarantee - # on the orientation of the edges, so our algorithm below must - # handle the case in which exactly one orientation, either (u, v) or - # (v, u), appears in this iterable. - edges = G.edges(nset1, data=data, default=default) - # If `nbunch2` is not provided, then it is assumed to be the set - # complement of `nbunch1`. For the sake of efficiency, this is - # implemented by using the `not in` operator, instead of by creating - # an additional set and using the `in` operator. - if nbunch2 is None: - return (e for e in edges if (e[0] in nset1) ^ (e[1] in nset1)) - nset2 = set(nbunch2) - return ( - e - for e in edges - if (e[0] in nset1 and e[1] in nset2) or (e[1] in nset1 and e[0] in nset2) - ) - - -""" -Union-find data structure. -""" - - -class UnionFind: - """Union-find data structure. - - Each unionFind instance X maintains a family of disjoint sets of - hashable objects, supporting the following two methods: - - - X[item] returns a name for the set containing the given item. - Each set is named by an arbitrarily-chosen one of its members; as - long as the set remains unchanged it will keep the same name. If - the item is not yet part of a set in X, a new singleton set is - created for it. - - - X.union(item1, item2, ...) merges the sets containing each item - into a single larger set. If any item is not yet part of a set - in X, it is added to X as one of the members of the merged set. - - Union-find data structure. Based on Josiah Carlson's code, - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/215912 - with significant additional changes by D. Eppstein. - http://www.ics.uci.edu/~eppstein/PADS/UnionFind.py - - """ - - def __init__(self, elements=None): - """Create a new empty union-find structure. - - If *elements* is an iterable, this structure will be initialized - with the discrete partition on the given set of elements. - - """ - if elements is None: - elements = () - self.parents = {} - self.weights = {} - for x in elements: - self.weights[x] = 1 - self.parents[x] = x - - def __getitem__(self, object): - """Find and return the name of the set containing the object.""" - - # check for previously unknown object - if object not in self.parents: - self.parents[object] = object - self.weights[object] = 1 - return object - - # find basic of objects leading to the root - path = [object] - root = self.parents[object] - while root != path[-1]: - path.append(root) - root = self.parents[root] - - # compress the basic and return - for ancestor in path: - self.parents[ancestor] = root - return root - - def __iter__(self): - """Iterate through all items ever found or unioned by this structure.""" - return iter(self.parents) - - def to_sets(self): - """Iterates over the sets stored in this structure. - - For example:: - - >>> partition = UnionFind("xyz") - >>> sorted(map(sorted, partition.to_sets())) - [['x'], ['y'], ['z']] - >>> partition.union("x", "y") - >>> sorted(map(sorted, partition.to_sets())) - [['x', 'y'], ['z']] - - """ - # Ensure fully pruned paths - - def groups(parents: dict): - sets = {} - for v, k in parents.items(): - if k not in sets: - sets[k] = set() - sets[k].add(v) - return sets - - for x in self.parents.keys(): - _ = self[x] # Evaluated for side-effect only - - yield from groups(self.parents).values() - - def union(self, *objects): - """Find the sets containing the objects and merge them all.""" - # Find the heaviest root according to its weight. - roots = iter(sorted({self[x] for x in objects}, key=lambda r: self.weights[r])) - try: - root = next(roots) - except StopIteration: - return - - for r in roots: - self.weights[root] += self.weights[r] - self.parents[r] = root -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/path/path.html b/docs/_modules/easygraph/functions/path/path.html deleted file mode 100644 index edca6ca4..00000000 --- a/docs/_modules/easygraph/functions/path/path.html +++ /dev/null @@ -1,372 +0,0 @@ - - - - - - easygraph.functions.path.path — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.path.path

-from easygraph.utils import *
-from easygraph.utils.decorators import *
-
-
-__all__ = [
-    "Dijkstra",
-    "Floyd",
-    "Prim",
-    "Kruskal",
-    "Spfa",
-    "single_source_bfs",
-    "single_source_dijkstra",
-    "multi_source_dijkstra",
-]
-
-
-
[docs]@hybrid("cpp_spfa") -def Spfa(G, node, weight="weight"): - raise EasyGraphError("Please input GraphC or DiGraphC.")
- - -
[docs]@not_implemented_for("multigraph") -def Dijkstra(G, node, weight="weight"): - """Returns the length of paths from the certain node to remaining nodes - - Parameters - ---------- - G : graph - weighted graph - node : int - - Returns - ------- - result_dict : dict - the length of paths from the certain node to remaining nodes - - Examples - -------- - Returns the length of paths from node 1 to remaining nodes - - >>> Dijkstra(G,node=1,weight="weight") - - """ - return single_source_dijkstra(G, node, weight=weight)
- - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -@hybrid("cpp_Floyd") -def Floyd(G, weight="weight"): - """Returns the length of paths from all nodes to remaining nodes - - Parameters - ---------- - G : graph - weighted graph - - Returns - ------- - result_dict : dict - the length of paths from all nodes to remaining nodes - - Examples - -------- - Returns the length of paths from all nodes to remaining nodes - - >>> Floyd(G,weight="weight") - - """ - adj = G.adj.copy() - result_dict = {} - for i in G: - result_dict[i] = {} - for i in G: - temp_key = adj[i].keys() - for j in G: - if j in temp_key: - result_dict[i][j] = adj[i][j].get(weight, 1) - else: - result_dict[i][j] = float("inf") - if i == j: - result_dict[i][i] = 0 - for k in G: - for i in G: - for j in G: - temp = result_dict[i][k] + result_dict[k][j] - if result_dict[i][j] > temp: - result_dict[i][j] = temp - return result_dict
- - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -@hybrid("cpp_Prim") -def Prim(G, weight="weight"): - """Returns the edges that make up the minimum spanning tree - - Parameters - ---------- - G : graph - weighted graph - - Returns - ------- - result_dict : dict - the edges that make up the minimum spanning tree - - Examples - -------- - Returns the edges that make up the minimum spanning tree - - >>> Prim(G,weight="weight") - - """ - adj = G.adj.copy() - result_dict = {} - for i in G: - result_dict[i] = {} - selected = [] - candidate = [] - for i in G: - if not selected: - selected.append(i) - else: - candidate.append(i) - while len(candidate): - start = None - end = None - min_weight = float("inf") - for i in selected: - for j in candidate: - if i in G and j in G[i] and adj[i][j].get(weight, 1) < min_weight: - start = i - end = j - min_weight = adj[i][j].get(weight, 1) - if start != None and end != None: - result_dict[start][end] = min_weight - selected.append(end) - candidate.remove(end) - else: - break - return result_dict
- - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -@hybrid("cpp_Kruskal") -def Kruskal(G, weight="weight"): - """Returns the edges that make up the minimum spanning tree - - Parameters - ---------- - G : graph - weighted graph - - Returns - ------- - result_dict : dict - the edges that make up the minimum spanning tree - - Examples - -------- - Returns the edges that make up the minimum spanning tree - - >>> Kruskal(G,weight="weight") - - """ - adj = G.adj.copy() - result_dict = {} - edge_list = [] - for i in G: - result_dict[i] = {} - for i in G: - for j in G[i]: - wt = adj[i][j].get(weight, 1) - edge_list.append([i, j, wt]) - edge_list.sort(key=lambda a: a[2]) - group = [[i] for i in G] - for edge in edge_list: - for i in range(len(group)): - if edge[0] in group[i]: - m = i - if edge[1] in group[i]: - n = i - if m != n: - result_dict[edge[0]][edge[1]] = edge[2] - group[m] = group[m] + group[n] - group[n] = [] - return result_dict
- - -
[docs]@not_implemented_for("multigraph") -def single_source_bfs(G, source, target=None): - nextlevel = {source: 0} - return dict(_single_source_bfs(G.adj, nextlevel, target=target))
- - -def _single_source_bfs(adj, firstlevel, target=None): - seen = {} - level = 0 - nextlevel = firstlevel - - while nextlevel: - thislevel = nextlevel - nextlevel = {} - for v in thislevel: - if v not in seen: - seen[v] = level - nextlevel.update(adj[v]) - yield (v, level) - if v == target: - break - level += 1 - del seen - - -
[docs]@not_implemented_for("multigraph") -def single_source_dijkstra(G, source, weight="weight", target=None): - from heapq import heappop - from heapq import heappush - - push = heappush - pop = heappop - adj = G.adj - dist = {} - seen = {} - from itertools import count - - c = count() - Q = [] - seen[source] = 0 - push(Q, (0, next(c), source)) - while Q: - (d, _, v) = pop(Q) - if v in dist: - continue - dist[v] = d - if v == target: - break - for u in adj[v]: - cost = adj[v][u].get(weight, 1) - vu_dist = dist[v] + cost - if u in dist: - if vu_dist < dist[u]: - raise ValueError("Contradictory paths found:", "negative weights?") - elif u not in seen or vu_dist < seen[u]: - seen[u] = vu_dist - push(Q, (vu_dist, next(c), u)) - else: - continue - return dist
- - -
[docs]@not_implemented_for("multigraph") -@hybrid("cpp_dijkstra_multisource") -def multi_source_dijkstra(G, sources, weight="weight", target=None): - return { - source: single_source_dijkstra(G, source, weight, target) for source in sources - }
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/AP_Greedy.html b/docs/_modules/easygraph/functions/structural_holes/AP_Greedy.html deleted file mode 100644 index 0dc5c43c..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/AP_Greedy.html +++ /dev/null @@ -1,515 +0,0 @@ - - - - - - easygraph.functions.structural_holes.AP_Greedy — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.AP_Greedy

-import math
-import random
-
-import easygraph as eg
-
-from easygraph.functions.components.biconnected import generator_articulation_points
-from easygraph.functions.components.connected import connected_components
-from easygraph.utils.decorators import *
-
-
-__all__ = ["common_greedy", "AP_Greedy"]
-
-
-
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -def common_greedy(G, k, c=1.0, weight="weight"): - """Common greedy method for structural hole spanners detection. - - Returns top k nodes as structural hole spanners, - Algorithm 1 of [1]_ - - Parameters - ---------- - G : easygraph.Graph - An undirected graph. - - k : int - top - k structural hole spanners - - c : float, optional (default : 1.0) - To define zeta: zeta = c * (n*n*n), and zeta is the large - value assigned as the shortest distance of two unreachable - vertices. - Default is 1. - - weight : String or None, optional (default : 'weight') - Key for edge weight. None if not concerning about edge weight. - - Returns - ------- - common_greedy : list - The list of each top-k structural hole spanners. - - See Also - -------- - AP_Greedy - - Examples - -------- - Returns the top k nodes as structural hole spanners, using **common_greedy**. - - >>> common_greedy(G, - ... k = 3, # To find top three structural holes spanners. - ... c = 1.0, # To define zeta: zeta = c * (n*n*n), and zeta is the large value assigned as the shortest distance of two unreachable vertices. - ... weight = 'weight') - - References - ---------- - .. [1] https://dl.acm.org/profile/81484650642 - - """ - v_sns = [] - G_i = G.copy() - N = len(G) - for i in range(k): - sorted_nodes = sort_nodes_by_degree(G_i, weight) - C_max = 0 - - for j in range(N - i): - G_i_j = G_i.copy() - G_i_j.remove_node(sorted_nodes[j]) - upper_bound = procedure1(G_i_j, c) - if upper_bound < C_max: - pass - else: - sum_all_shortest_paths = procedure2(G_i_j, c) - if sum_all_shortest_paths >= C_max: - v_i = sorted_nodes[j] - C_max = sum_all_shortest_paths - else: - pass - del G_i_j - - v_sns.append(v_i) - G_i.remove_node(v_i) - - del G_i - return v_sns
- - -def sort_nodes_by_degree(G, weight="weight"): - sorted_nodes = [] - for node, degree in sorted( - G.degree(weight=weight).items(), key=lambda x: x[1], reverse=True - ): - sorted_nodes.append(node) - return sorted_nodes - - -def procedure1(G, c=1.0): - """ - Procedure 1 of https://dl.acm.org/profile/81484650642 - - Parameters - ----------- - G : graph - - c : float - To define zeta: zeta = c * (n*n*n) - Default is 1. - - """ - components = connected_components(G) - upper_bound = 0 - for component in components: - component_subgraph = G.nodes_subgraph(from_nodes=list(component)) - spanning_tree = _get_spanning_tree_of_component(component_subgraph) - - random_root = list(spanning_tree.nodes)[ - random.randint(0, len(spanning_tree) - 1) - ] - num_subtree_nodes = _get_num_subtree_nodes(spanning_tree, random_root) - - N_tree = num_subtree_nodes[random_root] - for node, num in num_subtree_nodes.items(): - upper_bound += 2 * num * (N_tree - num) - - del component_subgraph, spanning_tree - - N_G = len(G) - zeta = c * math.pow(N_G, 3) - for component in components: - N_c = len(component) - upper_bound += N_c * (N_G - N_c) * zeta - - return upper_bound - - -def _get_spanning_tree_of_component(G): - spanning_tree = eg.Graph() - seen = set() - - def _plain_dfs(u): - for v, edge_data in G.adj[u].items(): - if v not in seen: - seen.add(v) - spanning_tree.add_edge(u, v) - _plain_dfs(v) - - random_node = list(G.nodes)[0] - seen.add(random_node) - spanning_tree.add_node(random_node) - - _plain_dfs(random_node) - - return spanning_tree - - -def _get_num_subtree_nodes(G, root): - num_subtree_nodes = dict() - seen = set() - - def _plain_dfs(u): - num_nodes = 1 - for v, edge_data in G.adj[u].items(): - if v not in seen: - seen.add(v) - num_nodes += _plain_dfs(v) - - num_subtree_nodes[u] = num_nodes - return num_nodes - - seen.add(root) - _plain_dfs(root) - - return num_subtree_nodes - - -def procedure2(G, c=1.0): - """ - Procedure 2 of https://dl.acm.org/profile/81484650642 - - Parameters - ----------- - G : graph - - c : float - To define zeta: zeta = c * (n*n*n) - Default is 1. - """ - components = connected_components(G) - C = 0 - N_G = len(G) - zeta = c * math.pow(N_G, 3) - for component in components: - component_subgraph = G.nodes_subgraph(from_nodes=list(component)) - C_l = _get_sum_all_shortest_paths_of_component(component_subgraph) - N_c = len(component) - C += C_l + N_c * (N_G - N_c) * zeta - - del component_subgraph - - return C - - -def _get_sum_all_shortest_paths_of_component(G): - # TODO: Using randomized algorithm in http://de.arxiv.org/pdf/1503.08528 - # instead of bfs method. - def _plain_bfs(G, source): - seen = {source} - nextlevel = {source} - level = 1 - sum_paths_of_G = 0 - - while nextlevel: - thislevel = nextlevel - nextlevel = set() - for u in thislevel: - for v in G.adj[u]: - if v not in seen: - seen.add(v) - nextlevel.add(v) - sum_paths_of_G += level - level += 1 - return sum_paths_of_G - - sum_paths = 0 - for node in G.nodes: - sum_paths += _plain_bfs(G, node) - - return sum_paths - - -
[docs]@not_implemented_for("multigraph") -@only_implemented_for_UnDirected_graph -def AP_Greedy(G, k, c=1.0, weight="weight"): - """AP greedy method for structural hole spanners detection. - - Returns top k nodes as structural hole spanners, - Algorithm 2 of [1]_ - - Parameters - ---------- - G : easygraph.Graph - An undirected graph. - - k : int - top - k structural hole spanners - - c : float, optional (default : 1.0) - To define zeta: zeta = c * (n*n*n), and zeta is the large - value assigned as the shortest distance of two unreachable - vertices. - Default is 1. - - weight : String or None, optional (default : 'weight') - Key for edge weight. None if not concerning about edge weight. - - Returns - ------- - AP_greedy : list - The list of each top-k structural hole spanners. - - Examples - -------- - Returns the top k nodes as structural hole spanners, using **AP_greedy**. - - >>> AP_greedy(G, - ... k = 3, # To find top three structural holes spanners. - ... c = 1.0, # To define zeta: zeta = c * (n*n*n), and zeta is the large value assigned as the shortest distance of two unreachable vertices. - ... weight = 'weight') - - References - ---------- - .. [1] https://dl.acm.org/profile/81484650642 - """ - v_sns = [] - G_i = G.copy() - N = len(G) - for i in range(k): - v_ap, lower_bound = _get_lower_bound_of_ap_nodes(G_i, c) - upper_bound = _get_upper_bound_of_non_ap_nodes(G_i, v_ap, c) - lower_bound = sorted(lower_bound.items(), key=lambda x: x[1], reverse=True) - - # print(upper_bound) - # print(lower_bound) - if len(lower_bound) != 0 and lower_bound[0][1] > max(upper_bound): - v_i = lower_bound[0][0] - else: # If acticulation points not chosen, use common_greedy instead. - sorted_nodes = sort_nodes_by_degree(G_i, weight) - C_max = 0 - - for j in range(N - i): - G_i_j = G_i.copy() - G_i_j.remove_node(sorted_nodes[j]) - upper_bound = procedure1(G_i_j, c) - if upper_bound < C_max: - pass - else: - sum_all_shortest_paths = procedure2(G_i_j, c) - if sum_all_shortest_paths >= C_max: - v_i = sorted_nodes[j] - C_max = sum_all_shortest_paths - else: - pass - del G_i_j - - v_sns.append(v_i) - G_i.remove_node(v_i) - - del G_i - return v_sns
- - -def _get_lower_bound_of_ap_nodes(G, c=1.0): - """ - Returns the articulation points and lower bound for each of them. - Procedure 3 of https://dl.acm.org/profile/81484650642 - - Parameters - ---------- - G : graph - An undirected graph. - - c : float - To define zeta: zeta = c * (n*n*n), and zeta is the large - value assigned as the shortest distance of two unreachable - vertices. - Default is 1. - """ - v_ap = [] - lower_bound = dict() - - N_G = len(G) - zeta = c * math.pow(N_G, 3) - components = connected_components(G) - for component in components: - component_subgraph = G.nodes_subgraph(from_nodes=list(component)) - articulation_points = list(generator_articulation_points(component_subgraph)) - N_component = len(component_subgraph) - for articulation in articulation_points: - component_subgraph_after_remove = component_subgraph.copy() - component_subgraph_after_remove.remove_node(articulation) - - lower_bound_value = 0 - lower_bound_value += sum( - (len(temp) * (N_G - len(temp))) for temp in components - ) - lower_bound_value += sum( - (len(temp) * (N_component - 1 - len(temp))) - for temp in connected_components(component_subgraph_after_remove) - ) - lower_bound_value += 2 * N_component - 2 * N_G - lower_bound_value *= zeta - - v_ap.append(articulation) - lower_bound[articulation] = lower_bound_value - - del component_subgraph_after_remove - - del component_subgraph - - return v_ap, lower_bound - - -def _get_upper_bound_of_non_ap_nodes(G, ap: list, c=1.0): - """ - Returns the upper bound value for each non-articulation points. - Eq.(14) of https://dl.acm.org/profile/81484650642 - - Parameters - ---------- - G : graph - An undirected graph. - - ap : list - Articulation points of G. - - c : float - To define zeta: zeta = c * (n*n*n), and zeta is the large - value assigned as the shortest distance of two unreachable - vertices. - Default is 1. - """ - upper_bound = [] - - N_G = len(G) - zeta = c * math.pow(N_G, 3) - components = connected_components(G) - for component in components: - non_articulation_points = component - set(ap) - for node in non_articulation_points: - upper_bound_value = 0 - upper_bound_value += sum( - (len(temp) * (N_G - len(temp))) for temp in components - ) - upper_bound_value += 2 * len(component) + 1 - 2 * N_G - upper_bound_value *= zeta - - upper_bound.append(upper_bound_value) - - return upper_bound -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/HAM.html b/docs/_modules/easygraph/functions/structural_holes/HAM.html deleted file mode 100644 index 5f1cd55b..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/HAM.html +++ /dev/null @@ -1,413 +0,0 @@ - - - - - - easygraph.functions.structural_holes.HAM — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.HAM

-__all__ = ["get_structural_holes_HAM"]
-from collections import Counter
-
-import numpy as np
-
-from easygraph.utils import *
-
-
-eps = 2.220446049250313e-16
-
-
-def sym(w):
-    import scipy.linalg as spl
-
-    """
-    Initialize a random orthogonal matrix F = w * (wT * w)^ (-1/2)
-    Parameters
-    ----------
-    w : A random matrix.
-
-    Returns
-    -------
-    F : a random orthogonal matrix.
-    """
-    return w.dot(spl.inv(spl.sqrtm(w.T.dot(w))))
-
-
-def avg_entropy(predicted_labels, actual_labels):
-    """
-    Calculate the average entropy between predicted_labels and actual_labels.
-
-    Parameters
-    ----------
-    predicted_labels : a Ndarray of predicted_labels.
-    actual_labels : a Ndarray of actual_labels.
-
-    Returns
-    -------
-    A float of average entropy.
-    """
-    import scipy.stats as stat
-
-    actual_labels_dict = {}
-    predicted_labels_dict = {}
-    for label in np.unique(actual_labels):
-        actual_labels_dict[label] = np.nonzero(actual_labels == label)[0]
-    for label in np.unique(predicted_labels):
-        predicted_labels_dict[label] = np.nonzero(predicted_labels == label)[0]
-    avg_value = 0
-    N = len(predicted_labels)
-    # store entropy for each community
-    for label, items in predicted_labels_dict.items():
-        N_i = float(len(items))
-        p_i = []
-        for label2, items2 in actual_labels_dict.items():
-            common = set(items.tolist()).intersection(set(items2.tolist()))
-            p_ij = float(len(common)) / N_i
-            p_i.append(p_ij)
-        entropy_i = stat.entropy(p_i)
-        avg_value += entropy_i * (N_i / float(N))
-    return avg_value
-
-
-def load_adj_matrix(G):
-    """
-    Transfer the graph into sparse matrix.
-    Parameters
-    ----------
-    G : graph
-        An undirected graph.
-
-    Returns
-    -------
-    A : A sparse matrix A
-    """
-    import scipy.sparse as sps
-
-    listE = []
-    for edge in G.edges:
-        listE.append(edge[0] - 1)
-        listE.append(edge[1] - 1)
-    adj_tuples = np.array(listE).reshape(-1, 2)
-    n = len(np.unique(adj_tuples))
-    vals = np.array([1] * len(G.edges))
-    max_id = max(max(adj_tuples[:, 0]), max(adj_tuples[:, 1])) + 1
-    A = sps.csr_matrix(
-        (vals, (adj_tuples[:, 0], adj_tuples[:, 1])), shape=(max_id, max_id)
-    )
-    A = A + A.T
-    return sps.csr_matrix(A)
-
-
-def majority_voting(votes):
-    """
-    majority voting.
-
-    Parameters
-    ----------
-    votes : a Ndarray of votes
-
-    Returns
-    -------
-    the most common label.
-    """
-    C = Counter(votes)
-    pairs = C.most_common(2)
-    if len(pairs) == 0:
-        return 0
-    if pairs[0][0] > 0:
-        return pairs[0][0]
-    elif len(pairs) > 1:
-        return pairs[1][0]
-    else:
-        return 0
-
-
-def label_by_neighbors(AdjMat, labels):
-    """
-    classifify SHS using majority voting.
-
-    Parameters
-    ----------
-    AdjMat : adjacency matrix
-    labels : a Ndarray of labeled communities of the nodes.
-
-    Returns
-    -------
-    labels : a Ndarray of labeled communities of the nodes.
-    """
-    assert AdjMat.shape[0] == len(labels), "dimensions are not equal"
-    unlabeled_idx = labels == 0
-    num_unlabeled = sum(unlabeled_idx)
-    count = 0
-    while num_unlabeled > 0:
-        idxs = np.array(np.nonzero(unlabeled_idx)[0])
-        next_labels = np.zeros(len(labels))
-        for idx in idxs:
-            neighbors = np.nonzero(AdjMat[idx, :] > 0)[1]
-            if len(neighbors) == 0:
-                next_labels[idx] = majority_voting(labels)
-            else:
-                neighbor_labels = labels[neighbors]
-                next_labels[idx] = majority_voting(neighbor_labels)
-        labels[idxs] = next_labels[idxs]
-        unlabeled_idx = labels == 0
-        num_unlabeled = sum(unlabeled_idx)
-    return labels
-
-
-
[docs]@not_implemented_for("multigraph") -def get_structural_holes_HAM(G, k, c, ground_truth_labels): - """Structural hole spanners detection via HAM method. - - Using HAM [1]_ to jointly detect SHS and communities. - - Parameters - ---------- - G : easygraph.Graph - An undirected graph. - - k : int - top - k structural hole spanners - - c : int - the number of communities - - ground_truth_labels : list of lists - The label of each node's community. - - Returns - ------- - top_k_nodes : list - The top-k structural hole spanners. - - SH_score : dict - The structural hole spanners score for each node, given by HAM. - - cmnt_labels : dict - The communities label of each node. - - - Examples - -------- - - >>> get_structural_holes_HAM(G, - ... k = 2, # To find top two structural holes spanners. - ... c = 2, - ... ground_truth_labels = [[0], [0], [1], [0], [1]] # The ground truth labels for each node - community detection result, for example. - ... ) - - References - ---------- - .. [1] https://dl.acm.org/doi/10.1145/2939672.2939807 - - """ - import scipy.linalg as spl - import scipy.sparse as sps - - from scipy.cluster.vq import kmeans - from scipy.cluster.vq import vq - from sklearn import metrics - - G_index, _, node_of_index = G.to_index_node_graph(begin_index=1) - - A_mat = load_adj_matrix(G_index) - A = A_mat # adjacency matrix - n = A.shape[0] # the number of nodes - - epsilon = 1e-4 # smoothing value: epsilon - max_iter = 50 # maximum iteration value - seeeed = 5433 - np.random.seed(seeeed) - topk = k - - # Inv of degree matrix D^-1 - invD = sps.diags((np.array(A.sum(axis=0))[0, :] + eps) ** (-1.0), 0) - # Laplacian matrix L = I - D^-1 * A - L = (sps.identity(n) - invD.dot(A)).tocsr() - # Initialize a random orthogonal matrix F - F = sym(np.random.random((n, c))) - - # Algorithm 1 - for step in range(max_iter): - Q = sps.identity(n).tocsr() - P = L.dot(F) - for i in range(n): - Q[i, i] = 0.5 / (spl.norm(P[i, :]) + epsilon) - - R = L.T.dot(Q).dot(L) - - W, V = np.linalg.eigh(R.todense()) - Wsort = np.argsort(W) # sort from smallest to largest - F = V[:, Wsort[0:c]] # select the smallest eigenvectors - - # find SH spanner - SH = np.zeros((n,)) - for i in range(n): - SH[i] = np.linalg.norm(F[i, :]) - SHrank = np.argsort(SH) # index of SH - - # METRICS BEGIN - - to_keep_index = np.sort(SHrank[topk:]) - A_temp = A[to_keep_index, :] - A_temp = A_temp[:, to_keep_index] - HAM_labels_keep = np.asarray(ground_truth_labels)[to_keep_index] - allLabels = np.asarray(ground_truth_labels) - - cluster_matrix = F - labelbook, distortion = kmeans(cluster_matrix[to_keep_index, :], c) - HAM_labels, dist = vq(cluster_matrix[to_keep_index, :], labelbook) - - print("AMI") - print( - "HAM: " - + str(metrics.adjusted_mutual_info_score(HAM_labels, HAM_labels_keep.T[0])) - ) - - # classifify SHS using majority voting - predLabels = np.zeros(len(ground_truth_labels)) - predLabels[to_keep_index] = HAM_labels + 1 - - HAM_predLabels = label_by_neighbors(A, predLabels) - print( - "HAM_all: " - + str(metrics.adjusted_mutual_info_score(HAM_predLabels, allLabels.T[0])) - ) - - print("NMI") - print( - "HAM: " - + str(metrics.normalized_mutual_info_score(HAM_labels, HAM_labels_keep.T[0])) - ) - print( - "HAM_all: " - + str(metrics.normalized_mutual_info_score(HAM_predLabels, allLabels.T[0])) - ) - - print("Entropy") - print("HAM: " + str(avg_entropy(HAM_labels, HAM_labels_keep.T[0]))) - print("HAM_all: " + str(avg_entropy(HAM_predLabels, allLabels.T[0]))) - - # METRICS END - - SH_score = dict() - for index, rank in enumerate(SHrank): - SH_score[node_of_index[index + 1]] = int(rank) - - cmnt_labels = dict() - for index, label in enumerate(HAM_predLabels): - cmnt_labels[node_of_index[index + 1]] = int(label) - - # top-k SHS - top_k_ind = np.argpartition(SHrank, -k)[-k:] - top_k_ind = top_k_ind[np.argsort(SHrank[top_k_ind])[::-1][:k]] - top_k_nodes = [] - for ind in top_k_ind: - top_k_nodes.append(node_of_index[ind + 1]) - - return top_k_nodes, SH_score, cmnt_labels
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/HIS.html b/docs/_modules/easygraph/functions/structural_holes/HIS.html deleted file mode 100644 index f442b63c..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/HIS.html +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - easygraph.functions.structural_holes.HIS — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.HIS

-import math
-
-from itertools import combinations
-from typing import List
-
-from easygraph.utils import *
-
-
-__all__ = ["get_structural_holes_HIS"]
-
-
-
[docs]@not_implemented_for("multigraph") -def get_structural_holes_HIS(G, C: List[frozenset], epsilon=1e-4, weight="weight"): - """Structural hole spanners detection via HIS method. - - Both **HIS** and **MaxD** are methods in [1]_. - The authors developed these two methods to find the structural holes spanners, - based on theory of information diffusion. - - Returns the value of `S`, `I`, `H` ,defined in **HIS** of [1], of each node in the graph. - Note that `H` quantifies the possibility that a node is a structural hole spanner. - To use `HIS` method, you should provide the community detection result as parameter. - - Parameters - ---------- - C : list of frozenset - Each frozenset denotes a community of nodes. - - epsilon : float - The threshold value. - - weight : string, optional (default : 'weight') - The key for edge weight. - - Returns - ------- - S : list of tuple - The `S` value in [1]_. - - I : float - The `I` value in [1]_. - - H : float - The `H` value in [1]_. - - See Also - -------- - MaxD - - Examples - -------- - - >>> get_structural_holes_HIS(G, - ... C = [frozenset([1,2,3]), frozenset([4,5,6])], # Two communities - ... epsilon = 0.01, - ... weight = 'weight' - ... ) - - - References - ---------- - .. [1] https://www.aminer.cn/structural-hole - - """ - # S: List[subset_index] - S = [] - for community_subset_size in range(2, len(C) + 1): - S.extend(list(combinations(range(len(C)), community_subset_size))) - # I: dict[node][cmnt_index] - # H: dict[node][subset_index] - I, H = initialize(G, C, S, weight=weight) - - alphas = [0.3 for i in range(len(C))] # list[cmnt_index] - betas = [(0.5 - math.pow(0.5, len(subset))) for subset in S] # list[subset_index] - - while True: - P = update_P(G, C, alphas, betas, S, I, H) # dict[node][cmnt_index] - I_new, H_new = update_I_H(G, C, S, P, I) - if is_convergence(G, C, I, I_new, epsilon): - break - else: - I, H = I_new, H_new - return S, I, H
- - -def initialize(G, C: List[frozenset], S: [tuple], weight="weight"): - I, H = dict(), dict() - for node in G.nodes: - I[node] = dict() - H[node] = dict() - - for node in G.nodes: - for index, community in enumerate(C): - if node in community: - # TODO: add PageRank or HITS to initialize I - I[node][index] = G.degree(weight=weight)[node] - else: - I[node][index] = 0 - - for node in G.nodes: - for index, subset in enumerate(S): - H[node][index] = min(I[node][i] for i in subset) - - return I, H - - -def update_P(G, C, alphas, betas, S, I, H): - P = dict() - for node in G.nodes: - P[node] = dict() - - for node in G.nodes: - for cmnt_index in range(len(C)): - subsets_including_current_cmnt = [] - for subset_index in range(len(S)): - if cmnt_index in S[subset_index]: - subsets_including_current_cmnt.append( - alphas[cmnt_index] * I[node][cmnt_index] - + betas[subset_index] * H[node][subset_index] - ) - P[node][cmnt_index] = max(subsets_including_current_cmnt) - return P - - -def update_I_H(G, C, S, P, I): - I_new, H_new = dict(), dict() - for node in G.nodes: - I_new[node] = dict() - H_new[node] = dict() - - for node in G.nodes: - for cmnt_index in range(len(C)): - P_max = max(P[neighbour][cmnt_index] for neighbour in G.adj[node]) - I_new[node][cmnt_index] = ( - P_max if (P_max > I[node][cmnt_index]) else I[node][cmnt_index] - ) - for subset_index, subset in enumerate(S): - H_new[node][subset_index] = min(I_new[node][i] for i in subset) - return I_new, H_new - - -def is_convergence(G, C, I, I_new, epsilon): - deltas = [] - for node in G.nodes: - for cmnt_index in range(len(C)): - deltas.append(abs(I[node][cmnt_index] - I_new[node][cmnt_index])) - return max(deltas) < epsilon -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/ICC.html b/docs/_modules/easygraph/functions/structural_holes/ICC.html deleted file mode 100644 index a648a54d..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/ICC.html +++ /dev/null @@ -1,411 +0,0 @@ - - - - - - easygraph.functions.structural_holes.ICC — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.ICC

-import easygraph as eg
-
-from easygraph.utils import *
-
-
-__all__ = ["ICC", "BICC", "AP_BICC"]
-
-
-def inverse_closeness_centrality(G, v):
-    c_v = sum(eg.Dijkstra(G, v).values()) / (len(G) - 1)
-    return c_v
-
-
-def bounded_inverse_closeness_centrality(G, v, l):
-    queue = []
-    queue.append(v)
-    seen = set()
-    seen.add(v)
-    shortest_path = eg.Floyd(G)
-    result = 0
-    while len(queue) > 0:
-        vertex = queue.pop(0)
-        if shortest_path[v][vertex] == l + 1:
-            break
-        nodes = G.neighbors(node=vertex)
-        for w in nodes:
-            if w not in seen:
-                queue.append(w)
-                seen.add(w)
-                result += shortest_path[v][w]
-    return result / (len(G) - 1)
-
-
-def Modified_DFS(G, u, V, time, n):
-    V[u]["color"] = "black"
-    time += 1
-    n -= 1
-    V[u]["discovered"] = time
-    V[u]["lowest"] = time
-    cc0 = n
-    V[u]["descendant"] = 0
-    root = u
-    for edge in G.edges:
-        u, v = edge[:2]
-        if V[u]["color"] == "white":
-            V[u]["color"] = "grey"
-            V[v]["parent"] = u
-            V[u]["child"] += 1
-            V, time, n = Modified_DFS(G, v, V, time, n)
-            V[u]["descendant"] = V[u]["descendant"] + V[v]["descendant"]
-            V[u]["lowest"] = min(V[u]["lowest"], V[v]["lowest"])
-            if V[v]["lowest"] >= V[u]["discovered"] or root == u and V[u]["child"] > 1:
-                V[u]["c"] += V[v]["descendant"] * (n - V[v]["descendant"] - 1)
-                cc0 -= V[v]["descendant"]
-        elif v != V[u]["parent"]:
-            V[u]["lowest"] = min(V[u]["lowest"], V[v]["discovered"])
-    V[u]["c"] += cc0 * (n - cc0 - 1)
-    return V, time, n
-
-
-def approximate_inverse_closeness_centrality(G):
-    V = {}
-    for i in G.nodes:
-        V[i] = {}
-        V[i]["child"] = 0
-        V[i]["color"] = "white"
-        V[i]["c"] = 0
-        V[i]["parent"] = None
-        V[i]["discovered"] = 0
-        V[i]["lowest"] = 0
-        V[i]["descendant"] = 0
-    time = 0
-    n = len(G)
-    for u in G.nodes:
-        if V[u]["color"] == "white":
-            V, time, n = Modified_DFS(G, u, V, time, n)
-    return V
-
-
-
[docs]@not_implemented_for("multigraph") -def ICC(G, k): - """an efficient algorithm for structural hole spanners detection. - - Returns top k nodes as structural hole spanners, - Algorithm 1 of [1]_ - - Parameters - ---------- - G : easygraph.Graph - An unweighted and undirected graph. - - k : int - top - k structural hole spanners - - Returns - ------- - V : list - The list of top-k structural hole spanners. - - Examples - -------- - Returns the top k nodes as structural hole spanners, using **ICC**. - - >>> ICC(G,k=3) - - References - ---------- - .. [1] https://dl.acm.org/doi/10.1145/2806416.2806431 - - """ - Q = [] - V = [] - for v in G.nodes: - i_c = inverse_closeness_centrality(G, v) - if len(Q) < k: - Q.append([v, i_c]) - continue - MAX = 0 - t = v - for i in Q: - if MAX < i[1]: - MAX = i[1] - t = i[0] - if i_c < MAX: - Q.remove([t, MAX]) - Q.append([v, i_c]) - for i in Q: - V.append(i[0]) - return V
- - -
[docs]@not_implemented_for("multigraph") -def BICC(G, k, K, l): - """an efficient algorithm for structural hole spanners detection. - - Returns top k nodes as structural hole spanners, - Algorithm 2 of [1]_ - - Parameters - ---------- - G : easygraph.Graph - An unweighted and undirected graph. - - k : int - top - k structural hole spanners - - K : int - the number of candidates K for the top-k hole spanners - - l : int - level-l neighbors of nodes - - Returns - ------- - V : list - The list of top-k structural hole spanners. - - Examples - -------- - Returns the top k nodes as structural hole spanners, using **BICC**. - - >>> BICC(G,k=3,K=5,l=4) - - References - ---------- - .. [1] https://dl.acm.org/doi/10.1145/2806416.2806431 - - """ - H = [] - V = [] - for v in G.nodes: - b_i_c = bounded_inverse_closeness_centrality(G, v, l) - if len(H) < K: - H.append([v, b_i_c]) - continue - MIN = 10000000 - t = v - for i in H: - if MIN > i[1]: - MIN = i[1] - t = i[0] - if b_i_c > MIN: - H.remove([t, MIN]) - H.append([v, b_i_c]) - for i in H: - v = i[0] - i_c = inverse_closeness_centrality(G, v) - if len(V) < k: - V.append([v, i_c]) - continue - MAX = 0 - t = v - for i in V: - if MAX < i[1]: - MAX = i[1] - t = i[0] - if i_c < MAX: - V.remove([t, MAX]) - V.append([v, i_c]) - VS = [] - for i in V: - VS.append(i[0]) - return VS
- - -
[docs]@not_implemented_for("multigraph") -def AP_BICC(G, k, K, l): - """an efficient algorithm for structural hole spanners detection. - - Returns top k nodes as structural hole spanners, - Algorithm 3 of [1]_ - - Parameters - ---------- - G : easygraph.Graph - An unweighted and undirected graph. - - k : int - top - k structural hole spanners - - K : int - the number of candidates K for the top-k hole spanners - - l : int - level-l neighbors of nodes - - Returns - ------- - V : list - The list of top-k structural hole spanners. - - Examples - -------- - Returns the top k nodes as structural hole spanners, using **AP_BICC**. - - >>> AP_BICC(G,k=3,K=5,l=4) - - References - ---------- - .. [1] https://dl.acm.org/doi/10.1145/2806416.2806431 - - """ - V = [] - T = [] - A = {} - A = approximate_inverse_closeness_centrality(G) - for v in A: - if len(T) < k: - T.append([v, A[v]["c"]]) - continue - MIN = 10000000 - t = v - for i in T: - if MIN > i[1]: - MIN = i[1] - t = i[0] - if A[v]["c"] > MIN: - T.remove([t, MIN]) - T.append([v, A[v]["c"]]) - if len(T) < k: - U = {} - for i in G.nodes: - if i not in A: - U.append(i) - kk = k - len(T) - Q = [] - for v in U: - b_i_c = bounded_inverse_closeness_centrality(G, v, l) - if len(Q) < K: - Q.append([v, b_i_c]) - else: - MIN = 10000000 - t = v - for i in Q: - if MIN > i[1]: - MIN = i[1] - t = i[0] - if b_i_c > MIN: - Q.remove([t, MIN]) - Q.append([v, b_i_c]) - while len(T) != k: - MAX = 0 - t = None - for i in Q: - if MAX < i[1]: - MAX = i[1] - t = i[0] - T.append([t, A[t]["c"]]) - for i in T: - V.append(i[0]) - return V
- - -if __name__ == "__main__": - G = eg.datasets.get_graph_karateclub() - print(ICC(G, 3)) - print(BICC(G, 3, 5, 3)) - print(AP_BICC(G, 3, 5, 3)) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/MaxD.html b/docs/_modules/easygraph/functions/structural_holes/MaxD.html deleted file mode 100644 index 7e0f74dc..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/MaxD.html +++ /dev/null @@ -1,560 +0,0 @@ - - - - - - easygraph.functions.structural_holes.MaxD — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.MaxD

-from typing import List
-
-from easygraph.utils import *
-
-
-__all__ = ["get_structural_holes_MaxD"]
-
-
-@not_implemented_for("multigraph")
-def get_community_kernel(G, C: List[frozenset], weight="weight"):
-    """
-    To get community kernels with most degrees.
-    Parameters
-    ----------
-    G : graph
-        An undirected graph.
-    C : int
-        #communities
-
-    Returns
-    -------
-    kernels
-    """
-    area = []
-    for i in range(len(G)):
-        area.append(0)
-    for i, cc in enumerate(C):
-        for each_node in cc:
-            area[each_node - 1] += 1 << i  # node_id from 1 to n.
-    kernels = []
-    cnt = 0
-    for i in range(len(C)):
-        mask = 1 << i
-        cnt += 1
-        q = []
-        p = []
-        for i in range(len(G)):
-            if (area[i] & mask) == mask:
-                q.append((G.degree(weight=weight)[i + 1], i + 1))
-        q.sort()
-        q.reverse()
-        for i in range(
-            max(int(len(q) / 100), min(2, len(q)))
-        ):  # latter of min for test.
-            p.append(q[i][1])
-        kernels.append(p)
-    if len(kernels) < 2:
-        print("ERROR: WE should have at least 2 communities.")
-    for i in range(len(kernels)):
-        if len(kernels[i]) == 0:
-            print("Community %d is too small." % i)
-            return None
-    return kernels
-
-
-
[docs]def get_structural_holes_MaxD(G, k, C: List[frozenset]): - """Structural hole spanners detection via MaxD method. - - Both **HIS** and **MaxD** are methods in [1]_. - The authors developed these two methods to find the structural holes spanners, - based on theory of information diffusion. - - Parameters - ---------- - - k : int - Top-`k` structural hole spanners - - C : list of frozenset - Each frozenset denotes a community of nodes. - - Returns - ------- - get_structural_holes_MaxD : list - Top-`k` structural hole spanners - - Examples - -------- - - >>> get_structural_holes_MaxD(G, - ... k = 5, # To find top five structural holes spanners. - ... C = [frozenset([1,2,3]), frozenset([4,5,6])] # Two communities - ... ) - - - References - ---------- - .. [1] https://www.aminer.cn/structural-hole - - """ - _init_data() - - G_index, index_of_node, node_of_index = G.to_index_node_graph(begin_index=1) - C_index = [] - for cmnt in C: - cmnt_index = [] - for node in cmnt: - cmnt_index.append(index_of_node[node]) - C_index.append(frozenset(cmnt_index)) - - kernels = get_community_kernel(G_index, C_index) - c = len(kernels) - save = [] - for i in range(len(G_index)): - save.append(False) - - build_network(kernels, c, G_index) - - n = len(G_index) - sflow = [] - save = [] - for i in range(n): - save.append(True) - q = [] - ans_list = [] - for step in range(k): - q.clear() - sflow.clear() - for i in range(n): - sflow.append(0) - max_flow(n, kernels, save) - for i in range(n * (c - 1)): - k_ = head[i] - while k_ >= 0: - if flow[k_] > 0: - sflow[i % n] += flow[k_] - k_ = nex[k_] - for i in range(n): - if save[i] == False: - q.append((-1, i)) - else: - q.append((sflow[i] + G_index.degree(weight="weight")[i + 1], i)) - q.sort() - q.reverse() - candidates = [] - for i in range(n): - if save[q[i][1]] == True and len(candidates) < k: - candidates.append(q[i][1]) - ret = pick_candidates(n, candidates, kernels, save) - ans_list.append(ret[1] + 1) - del sflow - del q - - for i in range(len(ans_list)): - ans_list[i] = node_of_index[ans_list[i]] - - return ans_list
- - -def pick_candidates(n, candidates, kernels, save): - """ - detect candidates. - Parameters - ---------- - n : #nodes - candidates : A list of candidates. - kernels : A list of kernels - save : A bool list of visited candidates for max_flow. - - Returns - ------- - A tuple of min_cut, best_candidate of this round. - """ - for i in range(len(candidates)): - save[candidates[i]] = False - old_flow = max_flow(n, kernels, save) - global prev_flow - prev_flow.clear() - for i in range(nedge): - prev_flow.append(flow[i]) - mcut = 100000000 - best_key = -1 - for i in range(len(candidates)): - key = candidates[i] - for j in range(len(candidates)): - save[candidates[j]] = True - save[key] = False - tp = max_flow(n, kernels, save, prev_flow) - if tp < mcut: - mcut = tp - best_key = key - for i in range(len(candidates)): - save[candidates[i]] = True - save[best_key] = False - return (old_flow + mcut, best_key) - - -head = [] - -point = [] -nex = [] -flow = [] -capa = [] - -dist = [] -work = [] -dsave = [] - -src = 0 -dest = 0 -node = 0 -nedge = 0 -prev_flow = [] -oo = 1000000000 - - -def _init_data(): - global head, point, nex, flow, capa - global dist, work, dsave - global src, dest, node, nedge, prev_flow, oo - - head = [] - - point = [] - nex = [] - flow = [] - capa = [] - - dist = [] - work = [] - dsave = [] - - src = 0 - dest = 0 - node = 0 - nedge = 0 - prev_flow = [] - oo = 1000000000 - - -def dinic_bfs(): - """ - using BFS to find augmenting basic. - - Returns - ------- - A bool, whether found a augmenting basic or not. - """ - global dist, dest, src, node - dist.clear() - for i in range(node): - dist.append(-1) - dist[src] = 0 - Q = [] - Q.append(src) - cl = 0 - while cl < len(Q): - k_ = Q[cl] - i = head[k_] - while i >= 0: - if flow[i] < capa[i] and dsave[point[i]] == True and dist[point[i]] < 0: - dist[point[i]] = dist[k_] + 1 - Q.append(point[i]) - i = nex[i] - cl += 1 - return dist[dest] >= 0 - - -def dinic_dfs(x, exp): - """ - using DFS to calc the augmenting basic and refresh network. - Parameters - ---------- - x : current node. - exp : current flow. - - Returns - ------- - current flow. - """ - if x == dest: - return exp - res = 0 - i = work[x] - global flow - while i >= 0: - v = point[i] - tmp = 0 - if flow[i] < capa[i] and dist[v] == dist[x] + 1: - tmp = dinic_dfs(v, min(exp, capa[i] - flow[i])) - if tmp > 0: - flow[i] += tmp - flow[i ^ 1] -= tmp - res += tmp - exp -= tmp - if exp == 0: - break - i = nex[i] - return res - - -def dinic_flow(): - """ - Dinic algorithm to calc max_flow. - - Returns - ------- - max_flow. - """ - result = 0 - global work - while dinic_bfs(): - work.clear() - for i in range(node): - work.append(head[i]) - result += dinic_dfs(src, oo) - return result - - -def max_flow(n, kernels, save, prev_flow=None): - """ - Calculate max_flow. - Parameters - ---------- - n : #nodes - kernels : A list of kernels. - save : A bool list of visited nodes. - prev_flow : A list of previous flows. - - Returns - ------- - max_flow - """ - global dsave, node - dsave.clear() - for i in range(node): - dsave.append(True) - - if prev_flow != None: - for i in range(nedge): - flow.append(prev_flow[i]) - else: - for i in range(nedge): - flow.append(0) - - c = len(kernels) - for i in range(n): - for k_ in range(c - 1): - dsave[k_ * n + i] = save[i] - ret = dinic_flow() - return ret - - -def init_MaxD(_node, _src, _dest): - """ - Initialize a network. - Parameters - ---------- - _node : #nodes - _src : the source node - _dest : the destiny node - - Returns - ------- - void - """ - global node, src, dest - node = _node - src = _src - dest = _dest - global point, capa, flow, nex, head - head.clear() - for i in range(node): - head.append(-1) - nedge = 0 - point.clear() - capa.clear() - flow.clear() - nex.clear() - - return - - -def addedge(u, v, c1, c2): - """ - Add an edge(u,v) with capacity c1 and inverse capacity c2. - Parameters - ---------- - u : node u - v : node v - c1 : capacity c1 - c2 : capacity c2 - - Returns - ------- - void - """ - global nedge - global point, capa, flow, nex, head - point.append(v) - capa.append(c1) - flow.append(0) - nex.append(head[u]) - head[u] = nedge - nedge += 1 - - point.append(u) - capa.append(c2) - flow.append(0) - nex.append(head[v]) - head[v] = nedge - nedge += 1 - return - - -def build_network(kernels, c, G): - """ - build a network. - Parameters - ---------- - kernels : A list of kernels. - c : #communities. - G : graph - An undirected graph. - - Returns - ------- - void - """ - n = len(G) - init_MaxD(n * (c - 1) + 2, n * (c - 1), n * (c - 1) + 1) - - base = 0 - for k_iter in range(c): - S1 = set() - S2 = set() - for i in range(c): - for j in range(len(kernels[i])): - if i == k_iter: - S1.add(kernels[i][j]) - elif i < k_iter: - S2.add(kernels[i][j]) - if len(S1) == 0 or len(S2) == 0: - continue - - for edges in G.edges: - addedge(base + edges[0] - 1, base + edges[1] - 1, 1, 1) - addedge(base + edges[1] - 1, base + edges[0] - 1, 1, 1) - - for i in S1: - if i not in S2: - addedge(src, base + i - 1, n, 0) - for i in S2: - if i not in S1: - addedge(base + i - 1, dest, n, 0) - base += n - return -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/NOBE.html b/docs/_modules/easygraph/functions/structural_holes/NOBE.html deleted file mode 100644 index c943c374..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/NOBE.html +++ /dev/null @@ -1,280 +0,0 @@ - - - - - - easygraph.functions.structural_holes.NOBE — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.NOBE

-import easygraph as eg
-import numpy as np
-
-from easygraph.utils import *
-
-
-__all__ = ["NOBE_SH", "NOBE_GA_SH"]
-
-
-
[docs]@not_implemented_for("multigraph") -def NOBE_SH(G, K, topk): - """detect SH spanners via NOBE[1]. - - Parameters - ---------- - G : easygraph.Graph - An unweighted and undirected graph. - - K : int - Embedding dimension k - - topk : int - top - k structural hole spanners - - Returns - ------- - SHS : list - The top-k structural hole spanners. - - Examples - -------- - >>> NOBE_SH(G,K=8,topk=5) - - References - ---------- - .. [1] https://www.researchgate.net/publication/325004496_On_Spectral_Graph_Embedding_A_Non-Backtracking_Perspective_and_Graph_Approximation - - """ - from sklearn.cluster import KMeans - - Y = eg.graph_embedding.NOBE(G, K) - dict = {} - a = 0 - for i in G.nodes: - dict[i] = a - a += 1 - if isinstance(Y[0, 0], complex): - Y = abs(Y) - kmeans = KMeans(n_clusters=K, random_state=0).fit(Y) - com = {} - cluster = {} - for i in dict: - com[i] = kmeans.labels_[dict[i]] - for i in com: - if com[i] in cluster: - cluster[com[i]].append(i) - else: - cluster[com[i]] = [] - cluster[com[i]].append(i) - vector = {} - for i in dict: - vector[i] = Y[dict[i]] - rds = RDS(com, cluster, vector, K) - rds_sort = sorted(rds.items(), key=lambda d: d[1], reverse=True) - SHS = list() - a = 0 - for i in rds_sort: - SHS.append(i[0]) - a += 1 - if a == topk: - break - return SHS
- - -
[docs]@not_implemented_for("multigraph") -def NOBE_GA_SH(G, K, topk): - """detect SH spanners via NOBE-GA[1]. - - Parameters - ---------- - G : easygraph.Graph - An unweighted and undirected graph. - - K : int - Embedding dimension k - - topk : int - top - k structural hole spanners - - Returns - ------- - SHS : list - The top-k structural hole spanners. - - Examples - -------- - >>> NOBE_GA_SH(G,K=8,topk=5) - - References - ---------- - .. [1] https://www.researchgate.net/publication/325004496_On_Spectral_Graph_Embedding_A_Non-Backtracking_Perspective_and_Graph_Approximation - - """ - from sklearn.cluster import KMeans - - Y = eg.NOBE_GA(G, K) - if isinstance(Y[0, 0], complex): - Y = abs(Y) - kmeans = KMeans(n_clusters=K, random_state=0).fit(Y) - com = {} - cluster = {} - a = 0 - for i in G.nodes: - com[i] = kmeans.labels_[a] - a += 1 - for i in com: - if com[i] in cluster: - cluster[com[i]].append(i) - else: - cluster[com[i]] = [] - cluster[com[i]].append(i) - vector = {} - a = 0 - for i in G.nodes: - vector[i] = Y[a] - a += 1 - rds = RDS(com, cluster, vector, K) - rds_sort = sorted(rds.items(), key=lambda d: d[1], reverse=True) - SHS = list() - a = 0 - for i in rds_sort: - SHS.append(i[0]) - a += 1 - if a == topk: - break - return SHS
- - -def RDS(com, cluster, vector, K): - rds = {} - Uc = {} - Rc = {} - for i in cluster: - sum_vec = np.zeros(K) - for j in cluster[i]: - sum_vec += vector[j] - Uc[i] = sum_vec / len(cluster[i]) - for i in cluster: - sum_dist = 0 - for j in cluster[i]: - sum_dist += np.linalg.norm(vector[j] - Uc[i]) - Rc[i] = sum_dist - for i in com: - maxx = 0 - fenzi = np.linalg.norm(vector[i] - Uc[com[i]]) / Rc[com[i]] - for j in cluster: - fenmu = np.linalg.norm(vector[i] - Uc[j]) / Rc[j] - if maxx < fenzi / fenmu: - maxx = fenzi / fenmu - rds[i] = maxx - return rds - - -if __name__ == "__main__": - G = eg.datasets.get_graph_karateclub() - print(NOBE_SH(G, K=2, topk=3)) - print(NOBE_GA_SH(G, K=2, topk=3)) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/SHII_metric.html b/docs/_modules/easygraph/functions/structural_holes/SHII_metric.html deleted file mode 100644 index 8e8bb5dc..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/SHII_metric.html +++ /dev/null @@ -1,407 +0,0 @@ - - - - - - easygraph.functions.structural_holes.SHII_metric — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.SHII_metric

-import math
-import random
-
-import easygraph as eg
-import numpy as np
-
-
-
[docs]class NodeParams: - def __init__(self, active, inWeight, threshold): - self.active = active - self.inWeight = inWeight - self.threshold = threshold
- - -
[docs]def structural_hole_influence_index( - G_original, - S, - C, - model, - variant=False, - seedRatio=0.05, - randSeedIter=10, - countIterations=100, - Directed=True, -): - """Returns the SHII metric of each seed. - - Parameters - ---------- - G_original: easygraph.Graph or easygraph.DiGraph - - S: list of int - A list of nodes which are structural hole spanners. - - C: list of list - Each list includes the nodes in one community. - - model: string - Propagation Model. Should be IC or LT. - - variant: bool, default is False - Whether returns variant SHII metrics or not. - variant SHII = # of the influenced outsider / # of the influenced insiders - SHII = # of the influenced outsiders / # of the total influenced nodes - - seedRatio: float, default is 0.05 - # of sampled seeds / # of nodes of the community that the given SHS belongs to. - - randSeedIter: int, default is 10 - How many iterations to sample seeds. - - countIterations: int default is 100 - Number of monte carlo simulations to be used. - - Directed: bool, default is True - Whether the graph is directed or not. - - Returns - ------- - seed_shii_pair : dict - the SHII metric of each seed - - Examples - -------- - # >>> structural_hole_influence_index(G, [3, 20, 9], Com, 'LT', seedRatio=0.1, Directed=False) - - References - ---------- - .. [1] https://dl.acm.org/doi/pdf/10.1145/2939672.2939807 - .. [2] https://github.com/LifangHe/KDD16_HAM/tree/master/SHII_metric - - """ - if not Directed: - G = eg.DiGraph() - for edge in G_original.edges: - G.add_edge(edge[0], edge[1]) - G.add_edge(edge[1], edge[0]) - else: - G = G_original.copy() - # form pair like {node_1:community_label_1,node_2:community_label_2} - node_label_pair = {} - for community_label in range(len(C)): - for node_i in range(len(C[community_label])): - node_label_pair[C[community_label][node_i]] = community_label - # print(node_label_pair) - seed_shii_pair = {} - for community_label in range(len(C)): - nodesInCommunity = [] - seedSetInCommunity = [] - for node in node_label_pair.keys(): - if node_label_pair[node] == community_label: - nodesInCommunity.append(node) - if node in S: - seedSetInCommunity.append(node) - - seedSetSize = int(math.ceil(len(nodesInCommunity) * seedRatio)) - - if len(seedSetInCommunity) == 0: - continue - - for seed in seedSetInCommunity: - print(">>>>>> processing seed ", seed, " now.") - oneSeedSet = [] - if node not in oneSeedSet: - oneSeedSet.append(seed) - seedNeighborSet = [] - # using BFS to add neighbors of the SH spanner to the seedNeighborSet as seed candidates - queue = [] - queue.append(seed) - while len(queue) > 0: - cur_node = queue[0] - count_neighbor = 0 - for neighbor in G.neighbors(node=cur_node): - if neighbor not in seedNeighborSet: - seedNeighborSet.append(neighbor) - count_neighbor = count_neighbor + 1 - if count_neighbor > 0: - if ( - len(queue) == 1 - and len(oneSeedSet) + len(seedNeighborSet) < seedSetSize - ): - for node in seedNeighborSet: - if node not in oneSeedSet: - oneSeedSet.append(node) - queue.append(node) - seedNeighborSet.clear() - queue.pop(0) - - avg_censor_score_1 = 0.0 - avg_censor_score_2 = 0.0 - - for randIter in range(randSeedIter): - if randIter % 5 == 0: - print("seed ", seed, ": ", randIter, " in ", randSeedIter) - randSeedSet = [] - for node in oneSeedSet: - randSeedSet.append(node) - seedNeighbors = [] - for node in seedNeighborSet: - seedNeighbors.append(node) - while len(seedNeighbors) > 0 and len(randSeedSet) < seedSetSize: - r = random.randint(0, len(seedNeighbors) - 1) - if seedNeighbors[r] not in randSeedSet: - randSeedSet.append(seedNeighbors[r]) - seedNeighbors.pop(r) - - if model == "IC": - censor_score_1, censor_score_2 = _independent_cascade( - G, - randSeedSet, - community_label, - countIterations, - node_label_pair, - ) - elif model == "LT": - censor_score_1, censor_score_2 = _linear_threshold( - G, - randSeedSet, - community_label, - countIterations, - node_label_pair, - ) - avg_censor_score_1 += censor_score_1 / randSeedIter - avg_censor_score_2 += censor_score_2 / randSeedIter - # print("seed ", seed, " avg_censor_score in ", randIter, "is ", censor_score_1 / randSeedIter) - if variant: - seed_shii_pair[seed] = avg_censor_score_2 - else: - seed_shii_pair[seed] = avg_censor_score_1 - return seed_shii_pair
- - -def _independent_cascade(G, S, community_label, countIterations, node_label_pair): - avg_result_1 = 0 - avg_result_2 = 0 - N = G.number_of_nodes() - for b in range(countIterations): - # print(b, " in ", countIterations) - p_vw = np.zeros((N, N)) # 节点被激活时,激活其它节点的概率,a对b的影响等于b对a的影响 - for random_i in range(N): - for random_j in range(random_i + 1, N): - num = random.random() - p_vw[random_i][random_j] = num - p_vw[random_j][random_i] = num - Q = [] - activeNodes = [] - for v in S: - Q.append(v) - activeNodes.append(v) - while len(Q) > 0: - v = Q[0] - for neighbor in G.neighbors(node=v): - if neighbor not in activeNodes: - toss = random.random() + 0.1 - if v <= 0 or neighbor <= 0: - print(v, neighbor) - # if toss>0.5: - # activeNodes.append(neighbor) - # Q.append(neighbor) - if toss >= p_vw[v - 1][neighbor - 1]: - activeNodes.append(neighbor) - Q.append(neighbor) - Q.pop(0) - self_cov = 0 - total_cov = 0 - uniqueActiveNodes = [] - for i in activeNodes: - if i not in uniqueActiveNodes: - uniqueActiveNodes.append(i) - for v in uniqueActiveNodes: - total_cov += 1 - if node_label_pair[v] == community_label: - self_cov += 1 - censor_score_1 = (total_cov - self_cov) / total_cov - censor_score_2 = (total_cov - self_cov) / self_cov - avg_result_1 += censor_score_1 / countIterations - avg_result_2 += censor_score_2 / countIterations - return avg_result_1, avg_result_2 - - -def _linear_threshold(G, S, community_label, countIterations, node_label_pair): - tol = 0.00001 - avg_result_1 = 0 - avg_result_2 = 0 - for b in range(countIterations): - activeNodes = [] - # T is the set of nodes that are to be processed - T = [] - Q = {} - for v in S: - activeNodes.append(v) - for neighbor in G.neighbors(node=v): - if neighbor not in S: - weight_degree = 1.0 / float(G.in_degree()[neighbor]) - if neighbor not in Q.keys(): - np = NodeParams(False, weight_degree, random.random()) - Q[neighbor] = np - T.append(neighbor) - else: - Q[neighbor].inWeight += weight_degree - - while len(T) > 0: - u = T[0] - if Q[u].inWeight >= Q[u].threshold + tol and not Q[u].active: - activeNodes.append(u) - Q[u].active = True - for neighbor in G.neighbors(node=u): - if neighbor in S: - continue - weight_degree = 1.0 / float(G.in_degree()[neighbor]) - if neighbor not in Q.keys(): - np = NodeParams(False, weight_degree, random.random()) - Q[neighbor] = np - T.append(neighbor) - else: - if not Q[neighbor].active: - T.append(neighbor) - Q[neighbor].inWeight += weight_degree - if Q[neighbor].inWeight - 1 > tol: - print("Error: the inweight for a node is > 1.") - T.pop(0) - - T.clear() - Q.clear() - - self_cov = 0 - total_cov = 0 - uniqueActiveNodes = [] - for i in activeNodes: - if i not in uniqueActiveNodes: - uniqueActiveNodes.append(i) - for v in uniqueActiveNodes: - total_cov += 1 - if node_label_pair[v] == community_label: - self_cov += 1 - censor_score_1 = (total_cov - self_cov) / total_cov # ==> SHII - censor_score_2 = (total_cov - self_cov) / self_cov - avg_result_1 += censor_score_1 / countIterations - avg_result_2 += censor_score_2 / countIterations - return avg_result_1, avg_result_2 - - -if __name__ == "__main__": - G = eg.datasets.get_graph_karateclub() - Com = [] - t1 = [1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 17, 18, 20, 22] - Com.append(t1) - t2 = [9, 10, 15, 16, 19, 21, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34] - Com.append(t2) - print("community_label:", Com) - result = structural_hole_influence_index( - G, [3, 20, 9], Com, "IC", seedRatio=0.1, Directed=False - ) - print(result) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/evaluation.html b/docs/_modules/easygraph/functions/structural_holes/evaluation.html deleted file mode 100644 index e74dd25a..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/evaluation.html +++ /dev/null @@ -1,509 +0,0 @@ - - - - - - easygraph.functions.structural_holes.evaluation — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.evaluation

-import math
-
-from easygraph.utils import *
-
-
-__all__ = ["effective_size", "efficiency", "constraint", "hierarchy"]
-
-
-def mutual_weight(G, u, v, weight=None):
-    try:
-        a_uv = G[u][v].get(weight, 1)
-    except KeyError:
-        a_uv = 0
-    try:
-        a_vu = G[v][u].get(weight, 1)
-    except KeyError:
-        a_vu = 0
-    return a_uv + a_vu
-
-
-sum_nmw_rec = {}
-max_nmw_rec = {}
-
-
-def normalized_mutual_weight(G, u, v, norm=sum, weight=None):
-    if norm == sum:
-        try:
-            return sum_nmw_rec[(u, v)]
-        except KeyError:
-            scale = norm(
-                mutual_weight(G, u, w, weight) for w in set(G.all_neighbors(u))
-            )
-            nmw = 0 if scale == 0 else mutual_weight(G, u, v, weight) / scale
-            sum_nmw_rec[(u, v)] = nmw
-            return nmw
-    elif norm == max:
-        try:
-            return max_nmw_rec[(u, v)]
-        except KeyError:
-            scale = norm(
-                mutual_weight(G, u, w, weight) for w in set(G.all_neighbors(u))
-            )
-            nmw = 0 if scale == 0 else mutual_weight(G, u, v, weight) / scale
-            max_nmw_rec[(u, v)] = nmw
-            return nmw
-
-
-def effective_size_parallel(nodes, G, weight):
-    ret = []
-    for node in nodes:
-        neighbors_of_node = set(G.all_neighbors(node))
-        if len(neighbors_of_node) == 0:
-            ret.append([node, float("nan")])
-            continue
-        ret.append(
-            [node, sum(redundancy(G, node, u, weight) for u in neighbors_of_node)]
-        )
-    return ret
-
-
-def effective_size_borgatti_parallel(nodes, G, weight):
-    ret = []
-    for node in nodes:
-        # Effective size is not defined for isolated nodes
-        if len(G[node]) == 0:
-            ret.append([node, float("nan")])
-            continue
-        E = G.ego_subgraph(node)
-        E.remove_node(node)
-        ret.append([node, len(E) - (2 * E.size()) / len(E)])
-    return ret
-
-
-def redundancy(G, u, v, weight=None):
-    nmw = normalized_mutual_weight
-    r = sum(
-        nmw(G, u, w, weight=weight) * nmw(G, v, w, norm=max, weight=weight)
-        for w in set(G.all_neighbors(u))
-    )
-    return 1 - r
-
-
-@not_implemented_for("multigraph")
-@hybrid("cpp_effective_size")
-def effective_size(G, nodes=None, weight=None, n_workers=None):
-    """Burt's metric - Effective Size.
-    Parameters
-    ----------
-    G : easygraph.Graph or easygraph.DiGraph
-    nodes : list of nodes or None, optional (default : None)
-        The nodes you want to calculate. If *None*, all nodes in `G` will be calculated.
-    weight : string or None, optional (default : None)
-        The key for edge weight. If *None*, `G` will be regarded as unweighted graph.
-    Returns
-    -------
-    effective_size : dict
-        The Effective Size of node in `nodes`.
-    Examples
-    --------
-    >>> effective_size(G,
-    ...                nodes=[1,2,3], # Compute the Effective Size of some nodes. The default is None for all nodes in G.
-    ...                weight='weight' # The weight key of the graph. The default is None for unweighted graph.
-    ...                )
-    References
-    ----------
-    .. [1] Burt R S. Structural holes: The social structure of competition[M].
-       Harvard university press, 2009.
-    """
-    sum_nmw_rec.clear()
-    max_nmw_rec.clear()
-    effective_size = {}
-    if nodes is None:
-        nodes = G
-    # Use Borgatti's simplified formula for unweighted and undirected graphs
-    if not G.is_directed() and weight is None:
-        if n_workers is not None:
-            import random
-
-            from functools import partial
-            from multiprocessing import Pool
-
-            local_function = partial(
-                effective_size_borgatti_parallel, G=G, weight=weight
-            )
-            nodes = list(nodes)
-            random.shuffle(nodes)
-            if len(nodes) > n_workers * 50000:
-                nodes = split_len(nodes, step=50000)
-            else:
-                nodes = split(nodes, n_workers)
-            with Pool(n_workers) as p:
-                ret = p.imap(local_function, nodes)
-                res = [x for i in ret for x in i]
-            effective_size = dict(res)
-        else:
-            for v in nodes:
-                # Effective size is not defined for isolated nodes
-                if len(G[v]) == 0:
-                    effective_size[v] = float("nan")
-                    continue
-                E = G.ego_subgraph(v)
-                E.remove_node(v)
-                effective_size[v] = len(E) - (2 * E.size()) / len(E)
-    else:
-        if n_workers is not None:
-            import random
-
-            from functools import partial
-            from multiprocessing import Pool
-
-            local_function = partial(effective_size_parallel, G=G, weight=weight)
-            nodes = list(nodes)
-            random.shuffle(nodes)
-            if len(nodes) > n_workers * 30000:
-                nodes = split_len(nodes, step=30000)
-            else:
-                nodes = split(nodes, n_workers)
-            with Pool(n_workers) as p:
-                ret = p.imap(local_function, nodes)
-                res = [x for i in ret for x in i]
-            effective_size = dict(res)
-        else:
-            for v in nodes:
-                # Effective size is not defined for isolated nodes
-                if len(G[v]) == 0:
-                    effective_size[v] = float("nan")
-                    continue
-                effective_size[v] = sum(
-                    redundancy(G, v, u, weight) for u in set(G.all_neighbors(v))
-                )
-    return effective_size
-
-
-
[docs]@not_implemented_for("multigraph") -def efficiency(G, nodes=None, weight=None): - """Burt's metric - Efficiency. - Parameters - ---------- - G : easygraph.Graph - nodes : list of nodes or None, optional (default : None) - The nodes you want to calculate. If *None*, all nodes in `G` will be calculated. - weight : string or None, optional (default : None) - The key for edge weight. If *None*, `G` will be regarded as unweighted graph. - Returns - ------- - efficiency : dict - The Efficiency of node in `nodes`. - Examples - -------- - >>> efficiency(G, - ... nodes=[1,2,3], # Compute the Efficiency of some nodes. The default is None for all nodes in G. - ... weight='weight' # The weight key of the graph. The default is None for unweighted graph. - ... ) - References - ---------- - .. [1] Burt R S. Structural holes: The social structure of competition[M]. - Harvard university press, 2009. - """ - e_size = effective_size(G, nodes=nodes, weight=weight) - degree = G.degree(weight=weight) - efficiency = {n: v / degree[n] for n, v in e_size.items()} - return efficiency
- - -def compute_constraint_of_nodes(nodes, G, weight): - ret = [] - for node in nodes: - neighbors_of_node = set(G.all_neighbors(node)) - if len(neighbors_of_node) == 0: - ret.append([node, float("nan")]) - continue - ret.append( - [node, sum(local_constraint(G, node, u, weight) for u in neighbors_of_node)] - ) - return ret - - -@not_implemented_for("multigraph") -@hybrid("cpp_constraint") -def constraint(G, nodes=None, weight=None, n_workers=None): - """Burt's metric - Constraint. - Parameters - ---------- - G : easygraph.Graph - nodes : list of nodes or None, optional (default : None) - The nodes you want to calculate. If *None*, all nodes in `G` will be calculated. - weight : string or None, optional (default : None) - The key for edge weight. If *None*, `G` will be regarded as unweighted graph. - workers : int or None, optional (default : None) - The number of workers calculating (default: None). - None if not using only one worker. - Returns - ------- - constraint : dict - The Constraint of node in `nodes`. - Examples - -------- - >>> constraint(G, - ... nodes=[1,2,3], # Compute the Constraint of some nodes. The default is None for all nodes in G. - ... weight='weight', # The weight key of the graph. The default is None for unweighted graph. - ... n_workers=4 # Parallel computing on four workers. The default is None for serial computing. - ... ) - References - ---------- - .. [1] Burt R S. Structural holes: The social structure of competition[M]. - Harvard university press, 2009. - """ - sum_nmw_rec.clear() - max_nmw_rec.clear() - local_constraint_rec.clear() - if nodes is None: - nodes = G.nodes - constraint = {} - - def compute_constraint_of_v(v): - neighbors_of_v = set(G.all_neighbors(v)) - if len(neighbors_of_v) == 0: - constraint_of_v = float("nan") - else: - constraint_of_v = sum( - local_constraint(G, v, n, weight) for n in neighbors_of_v - ) - return v, constraint_of_v - - if n_workers is not None: - import random - - from functools import partial - from multiprocessing import Pool - - local_function = partial(compute_constraint_of_nodes, G=G, weight=weight) - nodes = list(nodes) - random.shuffle(nodes) - if len(nodes) > n_workers * 30000: - nodes = split_len(nodes, step=30000) - else: - nodes = split(nodes, n_workers) - with Pool(n_workers) as p: - ret = p.imap(local_function, nodes) - constraint_results = [x for i in ret for x in i] - else: - constraint_results = [] - for v in nodes: - constraint_results.append(compute_constraint_of_v(v)) - - constraint = dict(constraint_results) - return constraint - - -local_constraint_rec = {} - - -def local_constraint(G, u, v, weight=None): - try: - return local_constraint_rec[(u, v)] - except KeyError: - nmw = normalized_mutual_weight - direct = nmw(G, u, v, weight=weight) - indirect = sum( - nmw(G, u, w, weight=weight) * nmw(G, w, v, weight=weight) - for w in set(G.all_neighbors(u)) - ) - result = (direct + indirect) ** 2 - local_constraint_rec[(u, v)] = result - return result - - -def hierarchy_parallel(nodes, G, weight): - ret = [] - for v in nodes: - E = G.ego_subgraph(v) - n = len(E) - 1 - C = 0 - c = {} - neighbors_of_v = set(G.all_neighbors(v)) - for w in neighbors_of_v: - C += local_constraint(G, v, w, weight) - c[w] = local_constraint(G, v, w, weight) - if n > 1: - ret.append( - [ - v, - sum( - c[w] / C * n * math.log(c[w] / C * n) / (n * math.log(n)) - for w in neighbors_of_v - ), - ] - ) - else: - ret.append([v, 0]) - - return ret - - -@not_implemented_for("multigraph") -@hybrid("cpp_hierarchy") -def hierarchy(G, nodes=None, weight=None, n_workers=None): - """Returns the hierarchy of nodes in the graph - Parameters - ---------- - G : graph - nodes : dict, optional (default: None) - weight : dict, optional (default: None) - Returns - ------- - hierarchy : dict - the hierarchy of nodes in the graph - Examples - -------- - Returns the hierarchy of nodes in the graph G - >>> hierarchy(G) - Reference - --------- - https://m.book118.com/html/2019/0318/5320024122002021.shtm - """ - sum_nmw_rec.clear() - max_nmw_rec.clear() - local_constraint_rec.clear() - if nodes is None: - nodes = G.nodes - hierarchy = {} - if n_workers is not None: - import random - - from functools import partial - from multiprocessing import Pool - - local_function = partial(hierarchy_parallel, G=G, weight=weight) - nodes = list(nodes) - random.shuffle(nodes) - if len(nodes) > n_workers * 30000: - nodes = split_len(nodes, step=30000) - else: - nodes = split(nodes, n_workers) - with Pool(n_workers) as p: - ret = p.imap(local_function, nodes) - res = [x for i in ret for x in i] - hierarchy = dict(res) - else: - for v in nodes: - E = G.ego_subgraph(v) - n = len(E) - 1 - C = 0 - c = {} - neighbors_of_v = set(G.all_neighbors(v)) - for w in neighbors_of_v: - C += local_constraint(G, v, w, weight) - c[w] = local_constraint(G, v, w, weight) - if n > 1: - hierarchy[v] = sum( - c[w] / C * n * math.log(c[w] / C * n) / (n * math.log(n)) - for w in neighbors_of_v - ) - if v not in hierarchy: - hierarchy[v] = 0 - return hierarchy -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/maxBlock.html b/docs/_modules/easygraph/functions/structural_holes/maxBlock.html deleted file mode 100644 index 53f22b5d..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/maxBlock.html +++ /dev/null @@ -1,724 +0,0 @@ - - - - - - easygraph.functions.structural_holes.maxBlock — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.maxBlock

-import math
-import random
-import sys
-
-import easygraph as eg
-
-from easygraph.functions.components.strongly_connected import condensation
-from easygraph.functions.components.strongly_connected import (
-    number_strongly_connected_components,
-)
-from easygraph.utils import *
-
-
-__all__ = [
-    "maxBlock",
-    "maxBlockFast",
-]
-tim = 0
-sys.setrecursionlimit(9000000)
-
-
-class dom_g:
-    def __init__(self, N, M):
-        self.tot = 0
-        self.h = []
-        self.ne = []
-        self.to = []
-        for i in range(N + 1):
-            self.h.append(0)
-        for i in range(max(N + 1, M + 1)):
-            self.ne.append(0)
-            self.to.append(0)
-
-    def add(self, x, y):
-        self.tot += 1
-        self.to[self.tot] = y
-        self.ne[self.tot] = self.h[x]
-        self.h[x] = self.tot
-
-
-def _tarjan(x, dfn, repos, g, fa):
-    global tim
-    tim += 1
-    dfn[x] = tim
-    repos[tim] = x
-    i = g.h[x]
-    while i:
-        if dfn[g.to[i]] == 0:
-            fa[g.to[i]] = x
-            _tarjan(g.to[i], dfn, repos, g, fa)
-        i = g.ne[i]
-
-
-def _find(x, f, dfn, semi, mi):
-    if x == f[x]:
-        return x
-    tmp = f[x]
-    f[x] = _find(f[x], f, dfn, semi, mi)
-    if dfn[semi[mi[tmp]]] < dfn[semi[mi[x]]]:
-        mi[x] = mi[tmp]
-    return f[x]
-
-
-def _dfs(x, tr, ans, desc_set):
-    ans[x] += 1
-    i = tr.h[x]
-
-    while i:
-        y = tr.to[i]
-        desc_set[x].add(y)
-        _dfs(y, tr, ans, desc_set)
-        ans[x] += ans[y]
-        for n in desc_set[y]:
-            desc_set[x].add(n)
-        i = tr.ne[i]
-
-
-def _get_idom(G, G_tr, node_s, ans_real, desc_set_real):
-    """Find the immediate dominator of each node and construct an s-rooted dominator tree.
-
-    Parameters
-    ----------
-    G: easygraph.DiGraph
-
-    G_tr: easygraph.DiGraph
-        an s-rooted dominator tree to be constructed.
-
-    node_s: int
-        the node s
-
-    ans_real: dict
-        denotes the number of proper descendants nu of each node u in the dominator tree.
-        a result to be calculated
-
-    desc_set_real: dict
-        denotes the set of proper descendants of node u in the dominator tree.
-        a result to be calculated
-
-    Examples
-    --------
-    # >>> G_tr = eg.DiGraph()
-    # >>> n_set = {}
-    # >>> desc_set = {}
-    # >>> _get_idom(G, G_tr, node_s, n_set, desc_set)
-
-    References
-    ----------
-    .. [1] http://keyblog.cn/article-173.html
-
-    """
-
-    global tim
-    tim = 0
-    n_dom = G.number_of_nodes()
-    m_dom = G.number_of_edges()
-    g = dom_g(n_dom + 1, m_dom + 1)
-    rg = dom_g(n_dom + 1, m_dom + 1)
-    ng = dom_g(n_dom + 1, m_dom + 1)
-    tr = dom_g(n_dom + 1, m_dom + 1)
-
-    dfn = [0 for i in range(n_dom + 1)]
-    repos = [0 for i in range(n_dom + 1)]
-    mi = [i for i in range(n_dom + 1)]
-    fa = [0 for i in range(n_dom + 1)]
-    f = [i for i in range(n_dom + 1)]
-    semi = [i for i in range(n_dom + 1)]
-    idom = [0 for i in range(n_dom + 1)]
-
-    # init
-    j = 0
-    node_map = {}
-    index_map = {}
-    for node in G.nodes:
-        j += 1
-        node_map[node] = j
-        index_map[j] = node
-
-    for edge in G.edges:
-        g.add(node_map[edge[0]], node_map[edge[1]])
-        rg.add(node_map[edge[1]], node_map[edge[0]])
-
-    # tarjan
-    _tarjan(node_map[node_s], dfn, repos, g, fa)
-    # work
-    i = n_dom
-    while i >= 2:
-        x = repos[i]
-        tmp = n_dom
-        j = rg.h[x]
-        while j:
-            if dfn[rg.to[j]] == 0:
-                j = rg.ne[j]
-                continue
-            if dfn[rg.to[j]] < dfn[x]:
-                tmp = min(tmp, dfn[rg.to[j]])
-            else:
-                _find(rg.to[j], f, dfn, semi, mi)
-                tmp = min(tmp, dfn[semi[mi[rg.to[j]]]])
-            j = rg.ne[j]
-
-        semi[x] = repos[tmp]
-        f[x] = fa[x]
-        ng.add(semi[x], x)
-        x = repos[i - 1]
-        j = ng.h[x]
-        while j:
-            y = ng.to[j]
-            _find(y, f, dfn, semi, mi)
-            if semi[mi[y]] == semi[y]:
-                idom[y] = semi[y]
-            else:
-                idom[y] = mi[y]
-            j = ng.ne[j]
-        i -= 1
-
-    i = 2
-    while i <= n_dom:
-        x = repos[i]
-        if x != 0:
-            if idom[x] != semi[x]:
-                idom[x] = idom[idom[x]]
-            tr.add(idom[x], x)
-            if x != node_map[node_s]:
-                G_tr.add_edge(index_map[idom[x]], index_map[x])
-        i += 1
-    G_tr.add_node(node_s)
-    ans = {}
-    desc_set = {}
-    for node in G_tr.nodes:
-        ans[node_map[node]] = 0
-        desc_set[node_map[node]] = set()
-
-    _dfs(node_map[node_s], tr, ans, desc_set)
-    for key in ans.keys():
-        ans[key] -= 1
-        ans_real[index_map[key]] = ans[key]
-    for key in desc_set.keys():
-        desc_set_real[index_map[key]] = set()
-        for value in desc_set[key]:
-            desc_set_real[index_map[key]].add(index_map[value])
-
-
-def _find_topk_shs_under_l(G, f_set, k, L):
-    """Find the top-k structural hole spanners under L simulations.
-
-    Parameters
-    ----------
-    G: easygraph.DiGraph
-
-    f_set: dict
-        user vi shares his/her information on network G at a rate fi.
-
-    k: int
-        top - k structural hole spanners.
-
-    L: int
-        the number of simulations.
-
-    Returns
-    -------
-    S_list : list
-        A set S of k nodes that block the maximum number of information propagations within L simulations.
-
-    ave_H_Lt_S: float
-        the average number of blocked information propagations by the nodes in set S with L t simulations.
-
-    """
-    h_set = {}
-    n = G.number_of_nodes()
-    for node in G.nodes:
-        h_set[node] = 0
-    for l in range(L):
-        if l % 100000 == 0:
-            print("[", l, "/", L, "] find topk shs under L")
-        # Choose a node s from the n nodes in G randomly
-        node_s = random.choice(list(G.nodes))
-        # Generate a graph G & = (V, E & ) from G under the live-edge graph model
-        G_live = G.copy()
-        for edge in G_live.edges:
-            wij = G_live[edge[0]][edge[1]]["weight"]
-            toss = random.random() + 0.1
-            if toss >= wij:
-                G_live.remove_edge(edge[0], edge[1])
-        # Obtain the induced subgraph by the set R G & (s ) of reachable nodes from s
-        R_set = eg.connected_component_of_node(G_live, node_s)
-        G_subgraph = eg.DiGraph()
-        for node in R_set:
-            G_subgraph.add_node(node)
-        for edge in G_live.edges:
-            if edge[0] in G_subgraph.nodes and edge[1] in G_subgraph.nodes:
-                G_subgraph.add_edge(edge[0], edge[1])
-        # Find the immediate dominator idom (v ) of each node v $ V && \ { s } in G
-        # Construct an s -rooted dominator tree
-        # Calculate the number of proper descendants n u of each node u $ V &&
-        G_tr = eg.DiGraph()
-        n_set = {}
-        desc_set = {}
-        _get_idom(G_subgraph, G_tr, node_s, n_set, desc_set)
-        for node_u in G_tr.nodes:
-            if node_u != node_s:
-                # the number of blocked information propagations by node u
-                h_set[node_u] += n_set[node_u] * f_set[node_s]
-    ave_H_set = {}
-    for node in G.nodes:
-        ave_H_set[node] = h_set[node] * n / L
-    ordered_set = sorted(ave_H_set.items(), key=lambda x: x[1], reverse=True)
-    S_list = []
-    ave_H_Lt_S = 0
-    for i in range(k):
-        S_list.append((ordered_set[i])[0])
-        ave_H_Lt_S += (ordered_set[i])[1]
-    return S_list, ave_H_Lt_S
-
-
-def _get_estimated_opt(G, f_set, k, c, delta):
-    """Estimation of the optimal value OPT.
-
-    Parameters
-    ----------
-    G: easygraph.DiGraph
-
-    f_set: dict
-        user vi shares his/her information on network G at a rate fi.
-
-    k: int
-        top - k structural hole spanners.
-
-    c: int
-        Success probability 1-n^-c of maxBlock.
-
-    delta: float
-        a small value delta > 0.
-
-    Returns
-    -------
-    res_opt : float
-        An approximate value OPT.
-
-    """
-    print("Estimating the optimal value OPT...")
-    n = G.number_of_nodes()
-    opt_ub = 0
-    for f_key in f_set.keys():
-        opt_ub = opt_ub + f_set[f_key]
-    opt_ub = opt_ub * k * (n - 1)
-    T = math.log((opt_ub / (delta / 2)), 2)
-    T = math.ceil(T)
-    lamda = 4 * (c * math.log(n, 2) + math.log(k * T, 2)) * (2 * k + 1) * k * n * n
-    for t in range(T):
-        opt_g = opt_ub / math.pow(2, t + 1)
-        L_t = math.ceil(lamda / opt_g)
-        print("[", t, "/", T, "] Estimating OPT: L=", L_t)
-        S_list, ave_H_Lt_S = _find_topk_shs_under_l(G, f_set, k, L_t)
-        if ave_H_Lt_S >= opt_g:
-            res_opt = opt_g / 2
-            return res_opt
-    print("[Warning] OPT is not greater that delta")
-    return -1
-
-
-def _find_separation_nodes(G):
-    G_s = condensation(G)
-    SCC_mapping = {}
-    incoming_info = G_s.graph["incoming_info"]
-    G_s_undirected = eg.Graph()
-    sep_nodes = set()
-    for node in (G_s.nodes).keys():
-        SCC_mapping[node] = G_s.nodes[node]["member"]
-        if len(G_s.nodes[node]["member"]) == 1:
-            sep_nodes.add(node)
-        G_s_undirected.add_node(node, member=G_s.nodes[node]["member"])
-    for edge in G_s.edges:
-        G_s_undirected.add_edge(edge[0], edge[1])
-    cut_nodes = eg.generator_articulation_points(G_s_undirected)
-    out_degree = G_s.out_degree()
-    in_degree = G_s.in_degree()
-    separations = set()
-    for cut_node in cut_nodes:
-        if cut_node in sep_nodes:
-            if out_degree[cut_node] >= 1 and in_degree[cut_node] >= 1:
-                CC_u = eg.connected_component_of_node(G_s_undirected, node=cut_node)
-                G_CC = G_s_undirected.nodes_subgraph(list(CC_u))
-                G_CC.remove_node(cut_node)
-                successors = G_s.neighbors(node=cut_node)
-                predecessors = G_s.predecessors(node=cut_node)
-                CC_removal = eg.connected_components(G_CC)
-                flag = True
-                for group in CC_removal:
-                    flag_succ = False
-                    flag_pred = False
-                    for node in group:
-                        if node in successors:
-                            flag_succ = True
-                            if flag_pred:
-                                flag = False
-                                break
-                        elif node in predecessors:
-                            flag_pred = True
-                            if flag_succ:
-                                flag = False
-                                break
-                    if not flag:
-                        break
-                if flag:
-                    separations.add(list(SCC_mapping[cut_node])[0])
-    return separations, SCC_mapping, incoming_info
-
-
-def _find_ancestors_of_node(G, node_t):
-    G_reverse = eg.DiGraph()
-    for node in G.nodes:
-        G_reverse.add_node(node)
-    for edge in G.edges:
-        G_reverse.add_edge(edge[1], edge[0])
-    node_dict = eg.Dijkstra(G_reverse, node=node_t)
-    ancestors = []
-    for node in G.nodes:
-        if node_dict[node] < float("inf") and node != node_t:
-            ancestors.append(node)
-    return ancestors
-
-
-
[docs]@not_implemented_for("multigraph") -def maxBlock(G, k, f_set=None, delta=1, eps=0.5, c=1, flag_weight=False): - """Structural hole spanners detection via maxBlock method. - - Parameters - ---------- - G: easygraph.DiGraph - - k: int - top - k structural hole spanners. - - f_set: dict, optional - user vi shares his/her information on network G at a rate fi. - default is a random [0,1) integer for each node - - delta: float, optional (default: 1) - a small value delta > 0. - - eps: float, optional (default: 0.5) - an error ratio eps with 0 < eps < 1. - - c: int, optional (default: 1) - Success probability 1-n^-c of maxBlock. - - flag_weight: bool, optional (default: False) - Denotes whether each edge has attribute 'weight' - - Returns - ------- - S_list : list - The list of each top-k structural hole spanners. - - See Also - ------- - maxBlockFast - - Examples - -------- - # >>> maxBlock(G, 100) - - References - ---------- - .. [1] https://doi.org/10.1016/j.ins.2019.07.072 - - """ - if f_set is None: - f_set = {} - for node in G.nodes: - f_set[node] = random.random() - if not flag_weight: - for edge in G.edges: - G[edge[0]][edge[1]]["weight"] = random.random() - n = G.number_of_nodes() - approximate_opt = _get_estimated_opt(G, f_set, k, c, delta) - print("approximate_opt:", approximate_opt) - L_min = (k + c) * math.log(n, 2) + math.log(4, 2) - L_min = L_min * k * n * n * math.pow(eps, -2) * (8 * k + 2 * eps) - L_min = L_min / approximate_opt - L_min = math.ceil(L_min) - print("L_min:", L_min) - S_list, ave_H_Lt_S = _find_topk_shs_under_l(G, f_set, k, L_min) - return S_list
- - -
[docs]@not_implemented_for("multigraph") -def maxBlockFast(G, k, f_set=None, L=None, flag_weight=False): - """Structural hole spanners detection via maxBlockFast method. - - Parameters - ---------- - G: easygraph.DiGraph - - G: easygraph.DiGraph - - k: int - top - k structural hole spanners. - - f_set: dict, optional - user vi shares his/her information on network G at a rate fi. - default is a random [0,1) integer for each node - - L: int, optional (default: log2n) - Simulation time L for maxBlockFast. - - flag_weight: bool, optional (default: False) - Denotes whether each edge has attribute 'weight' - - See Also - ------- - maxBlock - - Examples - -------- - # >>> maxBlockFast(G, 100) - - References - ---------- - .. [1] https://doi.org/10.1016/j.ins.2019.07.072 - - """ - h_set = {} - n = G.number_of_nodes() - if L is None: - L = math.ceil(math.log(n, 2)) - # print("L:", L) - if f_set is None: - f_set = {} - for node in G.nodes: - f_set[node] = random.random() - for node in G.nodes: - h_set[node] = 0 - if not flag_weight: - for edge in G.edges: - G[edge[0]][edge[1]]["weight"] = random.random() - for l in range(L): - if l % 10000 == 0: - print(l, "/", L, "...") - # Generate a graph G & = (V, E & ) from G under the live-edge graph model - G_live = G.copy() - for edge in G_live.edges: - wij = G_live[edge[0]][edge[1]]["weight"] - toss = random.random() + 0.1 - if toss >= wij: - G_live.remove_edge(edge[0], edge[1]) - - G0 = G_live.copy() - d_dict = {} - ns = number_strongly_connected_components(G0) - non_considered_nodes = set() - for node in G0.nodes: - d_dict[node] = 1 - non_considered_nodes.add(node) - G_p_1 = G0.copy() - for i in range(ns): - separation_nodes, SCC_mapping, incoming_info = _find_separation_nodes(G_p_1) - # print("separation_nodes:", separation_nodes) - if len(separation_nodes) > 0: - chosen_node = -1 - for node in separation_nodes: - node_dict = eg.Dijkstra(G_p_1, node=node) - flag = True - for other_sep in separation_nodes: - if other_sep != node: - if node_dict[other_sep] < float("inf"): - flag = False - break - if flag: - chosen_node = node - break - # print("chosen_node:", chosen_node) - G_tr = eg.DiGraph() - n_set = {} - desc_set = {} - _get_idom(G_p_1, G_tr, chosen_node, n_set, desc_set) - ancestors = _find_ancestors_of_node(G_p_1, chosen_node) - sum_fi = 0 - for node_av in ancestors: - sum_fi += f_set[node_av] - for node_u in G_tr.nodes: - D_u = 0 - for desc in desc_set[node_u]: - if desc not in d_dict.keys(): - print( - "Error: desc:", - desc, - "node_u", - node_u, - "d_dict:", - d_dict, - ) - print(desc_set[node_u]) - D_u += d_dict[desc] - if node_u != chosen_node: - h_set[node_u] += (f_set[chosen_node] + sum_fi) * D_u - elif node_u == chosen_node: - h_set[node_u] += sum_fi * D_u - d_dict[chosen_node] = 0 - for node_vj in G_tr.nodes: - d_dict[chosen_node] += d_dict[node_vj] - G_p = G_p_1.copy() - for neighbor in G_p_1.neighbors(node=chosen_node): - G_p.remove_edge(chosen_node, neighbor) - G_p_1 = G_p.copy() - non_considered_nodes.remove(chosen_node) - else: - V_set = set() - for key in SCC_mapping.keys(): - for node in SCC_mapping[key]: - if (node in non_considered_nodes) and ( - node not in incoming_info.keys() - ): - V_set.add(node) - if len(V_set) > 0: - break - # print("V_set:", V_set) - for node_v in V_set: - G_tr = eg.DiGraph() - n_set = {} - desc_set = {} - _get_idom(G_p_1, G_tr, node_v, n_set, desc_set) - for node_u in G_tr.nodes: - D_u = 0 - for desc in desc_set[node_u]: - if desc not in d_dict.keys(): - print( - "Error: desc:", - desc, - "node_u", - node_u, - "d_dict:", - d_dict, - ) - print(desc_set[node_u]) - D_u += d_dict[desc] - h_set[node_u] += f_set[node_v] * D_u - G_p = G_p_1.copy() - for node_v in V_set: - non_considered_nodes.remove(node_v) - for neighbor in G_p_1.neighbors(node=node_v): - G_p.remove_edge(node_v, neighbor) - G_p_1 = G_p.copy() - ave_H_set = {} - for node in G.nodes: - ave_H_set[node] = h_set[node] * n / L - ordered_set = sorted(ave_H_set.items(), key=lambda x: x[1], reverse=True) - S_list = [] - for i in range(k): - S_list.append((ordered_set[i])[0]) - return S_list
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/metrics.html b/docs/_modules/easygraph/functions/structural_holes/metrics.html deleted file mode 100644 index 2cc50b7f..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/metrics.html +++ /dev/null @@ -1,507 +0,0 @@ - - - - - - easygraph.functions.structural_holes.metrics — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.metrics

-import math
-import random
-
-import easygraph as eg
-import numpy as np
-
-from easygraph.utils import *
-
-
-__all__ = [
-    "sum_of_shortest_paths",
-    "nodes_of_max_cc_without_shs",
-    "structural_hole_influence_index",
-]
-
-
-
[docs]@not_implemented_for("multigraph") -def sum_of_shortest_paths(G, S): - r"""Returns the difference between the sum of lengths of all pairs shortest paths in G and the one in G\S. - The experiment metrics in [1]_ - - Parameters - ---------- - G: easygraph.Graph or easygraph.DiGraph - - S: list of int - A list of nodes witch are structural hole spanners. - - Returns - ------- - differ_between_sum : int - The difference between the sum of lengths of all pairs shortest paths in G and the one in G\S. - C(G/S)-C(G) - - Examples - -------- - >>> G_t=eg.datasets.get_graph_blogcatalog() - >>> S_t=eg.AP_Greedy(G_t, 10000) - >>> diff = sum_of_shortest_paths(G_t, S_t) - >>> print(diff) - - References - ---------- - .. [1] https://dl.acm.org/profile/81484650642 - - """ - mat_G = eg.Floyd(G) - sum_G = 0 - inf_const_G = math.ceil((G.number_of_nodes() ** 3) / 3) - for i in mat_G.values(): - for j in i.values(): - if math.isinf(j): - j = inf_const_G - sum_G += j - G_S = G.copy() - G_S.remove_nodes(S) - mat_G_S = eg.Floyd(G_S) - sum_G_S = 0 - inf_const_G_S = math.ceil((G_S.number_of_nodes() ** 3) / 3) - for i in mat_G_S.values(): - for j in i.values(): - if math.isinf(j): - j = inf_const_G_S - sum_G_S += j - return sum_G_S - sum_G
- - -
[docs]@not_implemented_for("multigraph") -def nodes_of_max_cc_without_shs(G, S): - r"""Returns the number of nodes in the maximum connected component in graph G\S. - The experiment metrics in [1]_ - - Parameters - ---------- - G: easygraph.Graph or easygraph.DiGraph - - S: list of int - A list of nodes witch are structural hole spanners. - - Returns - ------- - G_S_nodes_of_max_CC: int - The number of nodes in the maximum connected component in graph G\S. - - Examples - -------- - >>> G_t=eg.datasets.get_graph_blogcatalog() - >>> S_t=eg.AP_Greedy(G_t, 10000) - >>> maxx = nodes_of_max_cc_without_shs(G_t, S_t) - >>> print(maxx) - - References - ---------- - .. [1] https://dl.acm.org/profile/81484650642 - - """ - G_S = G.copy() - G_S.remove_nodes(S) - ccs = eg.connected_components(G_S) - max_num = 0 - for cc in ccs: - if len(cc) > max_num: - max_num = len(cc) - return max_num
- - -class NodeParams: - def __init__(self, active, inWeight, threshold): - self.active = active - self.inWeight = inWeight - self.threshold = threshold - - -
[docs]@not_implemented_for("multigraph") -def structural_hole_influence_index( - G_original, - S, - C, - model, - variant=False, - seedRatio=0.05, - randSeedIter=10, - countIterations=100, - Directed=True, -): - """Returns the SHII metric of each seed. - - Parameters - ---------- - G_original: easygraph.Graph or easygraph.DiGraph - - S: list of int - A list of nodes which are structural hole spanners. - - C: list of list - Each list includes the nodes in one community. - - model: string - Propagation Model. Should be IC or LT. - - variant: bool, default is False - Whether returns variant SHII metrics or not. - variant SHII = # of the influenced outsider / # of the influenced insiders - SHII = # of the influenced outsiders / # of the total influenced nodes - - seedRatio: float, default is 0.05 - # of sampled seeds / # of nodes of the community that the given SHS belongs to. - - randSeedIter: int, default is 10 - How many iterations to sample seeds. - - countIterations: int default is 100 - Number of monte carlo simulations to be used. - - Directed: bool, default is True - Whether the graph is directed or not. - - Returns - ------- - seed_shii_pair : dict - the SHII metric of each seed - - Examples - -------- - # >>> structural_hole_influence_index(G, [3, 20, 9], Com, 'LT', seedRatio=0.1, Directed=False) - - References - ---------- - .. [1] https://dl.acm.org/doi/pdf/10.1145/2939672.2939807 - .. [2] https://github.com/LifangHe/KDD16_HAM/tree/master/SHII_metric - - """ - if not Directed: - G = eg.DiGraph() - for edge in G_original.edges: - G.add_edge(edge[0], edge[1]) - G.add_edge(edge[1], edge[0]) - else: - G = G_original.copy() - # form pair like {node_1:community_label_1,node_2:community_label_2} - node_label_pair = {} - for community_label in range(len(C)): - for node_i in range(len(C[community_label])): - node_label_pair[C[community_label][node_i]] = community_label - # print(node_label_pair) - seed_shii_pair = {} - for community_label in range(len(C)): - nodesInCommunity = [] - seedSetInCommunity = [] - for node in node_label_pair.keys(): - if node_label_pair[node] == community_label: - nodesInCommunity.append(node) - if node in S: - seedSetInCommunity.append(node) - - seedSetSize = int(math.ceil(len(nodesInCommunity) * seedRatio)) - - if len(seedSetInCommunity) == 0: - continue - - for seed in seedSetInCommunity: - print(">>>>>> processing seed ", seed, " now.") - oneSeedSet = [] - if node not in oneSeedSet: - oneSeedSet.append(seed) - seedNeighborSet = [] - # using BFS to add neighbors of the SH spanner to the seedNeighborSet as seed candidates - queue = [] - queue.append(seed) - while len(queue) > 0: - cur_node = queue[0] - count_neighbor = 0 - for neighbor in G.neighbors(node=cur_node): - if neighbor not in seedNeighborSet: - seedNeighborSet.append(neighbor) - count_neighbor = count_neighbor + 1 - if count_neighbor > 0: - if ( - len(queue) == 1 - and len(oneSeedSet) + len(seedNeighborSet) < seedSetSize - ): - for node in seedNeighborSet: - if node not in oneSeedSet: - oneSeedSet.append(node) - queue.append(node) - seedNeighborSet.clear() - queue.pop(0) - - avg_censor_score_1 = 0.0 - avg_censor_score_2 = 0.0 - - for randIter in range(randSeedIter): - if randIter % 5 == 0: - print("seed ", seed, ": ", randIter, " in ", randSeedIter) - randSeedSet = [] - for node in oneSeedSet: - randSeedSet.append(node) - seedNeighbors = [] - for node in seedNeighborSet: - seedNeighbors.append(node) - while len(seedNeighbors) > 0 and len(randSeedSet) < seedSetSize: - r = random.randint(0, len(seedNeighbors) - 1) - if seedNeighbors[r] not in randSeedSet: - randSeedSet.append(seedNeighbors[r]) - seedNeighbors.pop(r) - - if model == "IC": - censor_score_1, censor_score_2 = _independent_cascade( - G, - randSeedSet, - community_label, - countIterations, - node_label_pair, - ) - elif model == "LT": - censor_score_1, censor_score_2 = _linear_threshold( - G, - randSeedSet, - community_label, - countIterations, - node_label_pair, - ) - avg_censor_score_1 += censor_score_1 / randSeedIter - avg_censor_score_2 += censor_score_2 / randSeedIter - # print("seed ", seed, " avg_censor_score in ", randIter, "is ", censor_score_1 / randSeedIter) - if variant: - seed_shii_pair[seed] = avg_censor_score_2 - else: - seed_shii_pair[seed] = avg_censor_score_1 - return seed_shii_pair
- - -def _independent_cascade(G, S, community_label, countIterations, node_label_pair): - avg_result_1 = 0 - avg_result_2 = 0 - N = G.number_of_nodes() - for b in range(countIterations): - # print(b, " in ", countIterations) - p_vw = np.zeros((N, N)) # 节点被激活时,激活其它节点的概率,a对b的影响等于b对a的影响 - for random_i in range(N): - for random_j in range(random_i + 1, N): - num = random.random() - p_vw[random_i][random_j] = num - p_vw[random_j][random_i] = num - Q = [] - activeNodes = [] - for v in S: - Q.append(v) - activeNodes.append(v) - while len(Q) > 0: - v = Q[0] - for neighbor in G.neighbors(node=v): - if neighbor not in activeNodes: - toss = random.random() + 0.1 - if v <= 0 or neighbor <= 0: - print(v, neighbor) - # if toss>0.5: - # activeNodes.append(neighbor) - # Q.append(neighbor) - if toss >= p_vw[v - 1][neighbor - 1]: - activeNodes.append(neighbor) - Q.append(neighbor) - Q.pop(0) - self_cov = 0 - total_cov = 0 - uniqueActiveNodes = [] - for i in activeNodes: - if i not in uniqueActiveNodes: - uniqueActiveNodes.append(i) - for v in uniqueActiveNodes: - total_cov += 1 - if node_label_pair[v] == community_label: - self_cov += 1 - censor_score_1 = (total_cov - self_cov) / total_cov - censor_score_2 = (total_cov - self_cov) / self_cov - avg_result_1 += censor_score_1 / countIterations - avg_result_2 += censor_score_2 / countIterations - return avg_result_1, avg_result_2 - - -def _linear_threshold(G, S, community_label, countIterations, node_label_pair): - tol = 0.00001 - avg_result_1 = 0 - avg_result_2 = 0 - for b in range(countIterations): - activeNodes = [] - # T is the set of nodes that are to be processed - T = [] - Q = {} - for v in S: - activeNodes.append(v) - for neighbor in G.neighbors(node=v): - if neighbor not in S: - weight_degree = 1.0 / float(G.in_degree()[neighbor]) - if neighbor not in Q.keys(): - np = NodeParams(False, weight_degree, random.random()) - Q[neighbor] = np - T.append(neighbor) - else: - Q[neighbor].inWeight += weight_degree - - while len(T) > 0: - u = T[0] - if Q[u].inWeight >= Q[u].threshold + tol and not Q[u].active: - activeNodes.append(u) - Q[u].active = True - for neighbor in G.neighbors(node=u): - if neighbor in S: - continue - weight_degree = 1.0 / float(G.in_degree()[neighbor]) - if neighbor not in Q.keys(): - np = NodeParams(False, weight_degree, random.random()) - Q[neighbor] = np - T.append(neighbor) - else: - if not Q[neighbor].active: - T.append(neighbor) - Q[neighbor].inWeight += weight_degree - if Q[neighbor].inWeight - 1 > tol: - print("Error: the inweight for a node is > 1.") - T.pop(0) - - T.clear() - Q.clear() - - self_cov = 0 - total_cov = 0 - uniqueActiveNodes = [] - for i in activeNodes: - if i not in uniqueActiveNodes: - uniqueActiveNodes.append(i) - for v in uniqueActiveNodes: - total_cov += 1 - if node_label_pair[v] == community_label: - self_cov += 1 - censor_score_1 = (total_cov - self_cov) / total_cov # ==> SHII - censor_score_2 = (total_cov - self_cov) / self_cov - avg_result_1 += censor_score_1 / countIterations - avg_result_2 += censor_score_2 / countIterations - return avg_result_1, avg_result_2 - - -if __name__ == "__main__": - G = eg.datasets.get_graph_karateclub() - Com = [] - t1 = [1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 17, 18, 20, 22] - Com.append(t1) - t2 = [9, 10, 15, 16, 19, 21, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34] - Com.append(t2) - print("community_label:", Com) - result = structural_hole_influence_index( - G, [3, 20, 9], Com, "IC", seedRatio=0.1, Directed=False - ) - print(result) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/functions/structural_holes/weakTie.html b/docs/_modules/easygraph/functions/structural_holes/weakTie.html deleted file mode 100644 index 8fb86436..00000000 --- a/docs/_modules/easygraph/functions/structural_holes/weakTie.html +++ /dev/null @@ -1,451 +0,0 @@ - - - - - - easygraph.functions.structural_holes.weakTie — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.functions.structural_holes.weakTie

-import easygraph as eg
-
-from easygraph.utils import *
-
-
-__all__ = [
-    "weakTie",
-    "weakTieLocal",
-]
-
-
-def _computeTieStrength(G, node_u, node_v):
-    F_u = set(G.neighbors(node=node_u))
-    F_u.add(node_u)
-    F_v = set(G.neighbors(node=node_v))
-    F_v.add(node_v)
-    uni = len(F_u.union(F_v))
-    inter = len(F_u.intersection(F_v))
-    S_uv = inter / uni
-    G[node_u][node_v]["strength"] = S_uv
-
-
-def _computeAllTieStrength(G):
-    for edge in G.edges:
-        node_u = edge[0]
-        node_v = edge[1]
-        _computeTieStrength(G, node_u, node_v)
-    # print(G.edges)
-
-
-def _strongly_connected_components(G, threshold):
-    """Generate nodes in strongly connected components of graph with constraint threshold.
-
-    Parameters
-    ----------
-    G : easygraph.DiGraph
-        A directed graph.
-
-    threshold: float
-        the edge whose tie strength is smaller than threshold will be ignored.
-
-    Returns
-    -------
-    comp : generator of sets
-        A generator of sets of nodes, one for each strongly connected
-        component of G.
-
-    Examples
-    --------
-    # >>> _strongly_connected_components(G, 0.2)
-
-    Notes
-    -----
-    Uses Tarjan's algorithm[1]_ with Nuutila's modifications[2]_.
-    Nonrecursive version of algorithm.
-
-    References
-    ----------
-    .. [1] Depth-first search and linear graph algorithms, R. Tarjan
-       SIAM Journal of Computing 1(2):146-160, (1972).
-
-    .. [2] On finding the strongly connected components in a directed graph.
-       E. Nuutila and E. Soisalon-Soinen
-       Information Processing Letters 49(1): 9-14, (1994)..
-
-    """
-    preorder = {}
-    lowlink = {}
-    scc_found = set()
-    scc_queue = []
-    i = 0  # Preorder counter
-    for source in G:
-        if source not in scc_found:
-            queue = [source]
-            while queue:
-                v = queue[-1]
-                if v not in preorder:
-                    i = i + 1
-                    preorder[v] = i
-                done = True
-                for w in G[v]:
-                    if G[v][w]["strength"] >= threshold:
-                        if w not in preorder:
-                            queue.append(w)
-                            done = False
-                            break
-                if done:
-                    lowlink[v] = preorder[v]
-                    for w in G[v]:
-                        if G[v][w]["strength"] >= threshold:
-                            if w not in scc_found:
-                                if preorder[w] > preorder[v]:
-                                    lowlink[v] = min([lowlink[v], lowlink[w]])
-                                else:
-                                    lowlink[v] = min([lowlink[v], preorder[w]])
-                    queue.pop()
-                    if lowlink[v] == preorder[v]:
-                        scc = {v}
-                        while scc_queue and preorder[scc_queue[-1]] > preorder[v]:
-                            k = scc_queue.pop()
-                            scc.add(k)
-                        scc_found.update(scc)
-                        yield scc
-                    else:
-                        scc_queue.append(v)
-
-
-def _computeCloseness(G, c, u, threshold, length):
-    n = 0
-    strength_sum_u = 0
-    for v in c:
-        if u in G[v] and v != u:
-            if G[v][u]["strength"] != 0:
-                n += 1
-                strength_sum_u += G[v][u]["strength"]
-    closeness_c_u = (strength_sum_u - n * threshold) / length
-    return closeness_c_u
-
-
-def _computeScore(G, threshold):
-    score_dict = {}
-    for node in G.nodes:
-        score_dict[node] = 0
-    for c in _strongly_connected_components(G, threshold):
-        length = len(c)
-        for u in G.nodes:
-            closeness_c_u = _computeCloseness(G, c, u, threshold, length)
-            if closeness_c_u < 0:
-                score_dict[u] += (-1) * closeness_c_u
-    return score_dict
-
-
-
[docs]@not_implemented_for("multigraph") -def weakTie(G, threshold, k): - """Return top-k nodes with highest scores which were computed by WeakTie method. - - Parameters - ---------- - G: easygraph.DiGraph - - k: int - top - k nodes with highest scores. - - threshold: float - tie strength threshold. - - Returns - ------- - SHS_list : list - The list of each nodes with highest scores. - - score_dict: dict - The score of each node, can be used for WeakTie-Local and WeakTie-Bi. - - See Also - ------- - weakTieLocal - - Examples - -------- - # >>> SHS_list,score_dict=weakTie(G, 0.2, 3) - - References - ---------- - .. [1] Mining Brokers in Dynamic Social Networks. Chonggang Song, Wynne Hsu, Mong Li Lee. Proc. of ACM CIKM, 2015. - - """ - _computeAllTieStrength(G) - score_dict = _computeScore(G, threshold) - ordered_set = sorted(score_dict.items(), key=lambda x: x[1], reverse=True) - SHS_list = [] - for i in range(k): - SHS_list.append((ordered_set[i])[0]) - print("score dict:", score_dict) - print("top-k nodes:", SHS_list) - return SHS_list, score_dict
- - -@not_implemented_for("multigraph") -def _updateScore(u, G, threshold): - score_u = 0 - for c in _strongly_connected_components(G, threshold): - length = len(c) - closeness_c_u = _computeCloseness(G, c, u, threshold, length) - if closeness_c_u < 0: - score_u -= closeness_c_u - return score_u - - -def _get2hop(G, node): - neighbors = [] - firstlevel = {node: 1} - seen = {} # level (number of hops) when seen in BFS - level = 0 # the current level - nextlevel = set(firstlevel) # set of nodes to check at next level - n = len(G.adj) - while nextlevel and level <= 2: - thislevel = nextlevel # advance to next level - nextlevel = set() # and start a new set (fringe) - found = [] - for v in thislevel: - if v not in seen: - seen[v] = level # set the level of vertex v - found.append(v) - # yield (v, level) - neighbors.append(v) - if len(seen) == n: - return - for v in found: - nextlevel.update(G.adj[v]) - level += 1 - del seen - return neighbors - - -def _commonUpdate(G, node_u, node_v, threshold, score_dict): - for node_w in G.neighbors(node=node_u): - _computeTieStrength(G, node_u, node_w) - for node_w in G.predecessors(node=node_u): - _computeTieStrength(G, node_w, node_u) - G_un = eg.Graph() - for node in G.nodes: - G_un.add_node(node) - for edge in G.edges: - if not G_un.has_edge(edge[0], edge[1]): - G_un.add_edge(edge[0], edge[1]) - u_2hop = _get2hop(G_un, node_u) - G_u = G.nodes_subgraph(from_nodes=u_2hop) - v_2hop = _get2hop(G_un, node_v) - G_v = G.nodes_subgraph(from_nodes=v_2hop) - score_u = _updateScore(node_u, G_u, threshold) - score_v = _updateScore(node_v, G_v, threshold) - score_dict[node_u] = score_u - score_dict[node_v] = score_v - all_neigh_u = list(set(G.all_neighbors(node=node_u))) - # print("all_neigh:",all_neigh_u) - all_neigh_v = list(set(G.all_neighbors(node=node_v))) - for node_w in all_neigh_u: - if node_w in all_neigh_v: - w_2hop = _get2hop(G_un, node_w) - G_w = G.nodes_subgraph(from_nodes=w_2hop) - score_w = _updateScore(node_w, G_w, threshold) - else: - score_w = 0 - w_2hop = _get2hop(G_un, node_w) - G_w = G.nodes_subgraph(from_nodes=w_2hop) - for c in _strongly_connected_components(G_w, threshold): - if node_u in c: - length = len(c) - closeness_c_w = _computeCloseness(G, c, node_w, threshold, length) - if closeness_c_w < 0: - score_w -= closeness_c_w - score_dict[node_w] = score_w - - -
[docs]def weakTieLocal(G, edges_plus, edges_delete, threshold, score_dict, k): - """Find brokers in evolving social networks, utilize the 2-hop neighborhood of an affected node to identify brokers. - - Parameters - ---------- - G: easygraph.DiGraph - - edges_plus: list of list - set of edges to be added - - edges_delete: list of list - set of edges to be removed - - threshold: float - tie strength threshold. - - score_dict: dict - The score of each node computed before. - - k: int - top - k nodes with highest scores. - - Returns - ------- - SHS_list : list - The list of each nodes with highest scores. - - See Also - ------- - weakTie - - Examples - -------- - # >>> SHS_list=weakTieLocal(G, [[2, 7]], [[1,3]], 0.2, score_dict, 3) - - References - ---------- - .. [1] Mining Brokers in Dynamic Social Networks. Chonggang Song, Wynne Hsu, Mong Li Lee. Proc. of ACM CIKM, 2015. - - """ - for edge in edges_plus: - G.add_edge(edge[0], edge[1]) - _computeTieStrength(G, edge[0], edge[1]) - _commonUpdate(G, edge[0], edge[1], threshold, score_dict) - for edge in edges_delete: - G.remove_edge(edge[0], edge[1]) - _commonUpdate(G, edge[0], edge[1], threshold, score_dict) - ordered_set = sorted(score_dict.items(), key=lambda x: x[1], reverse=True) - SHS_list = [] - for i in range(k): - SHS_list.append((ordered_set[i])[0]) - print("updated score:", score_dict) - print("top-k nodes:", SHS_list) - return SHS_list
- - -if __name__ == "__main__": - G = eg.DiGraph() - G.add_edge(1, 5) - G.add_edge(1, 4) - G.add_edge(2, 1) - G.add_edge(2, 6) - G.add_edge(2, 9) - G.add_edge(3, 4) - G.add_edge(3, 1) - G.add_edge(4, 3) - G.add_edge(4, 1) - G.add_edge(4, 5) - G.add_edge(5, 4) - G.add_edge(5, 8) - G.add_edge(6, 1) - G.add_edge(6, 2) - G.add_edge(7, 2) - G.add_edge(7, 3) - G.add_edge(7, 10) - G.add_edge(8, 4) - G.add_edge(8, 5) - G.add_edge(9, 6) - G.add_edge(9, 10) - G.add_edge(10, 7) - G.add_edge(10, 9) - SHS_list, score_dict = weakTie(G, 0.2, 3) - SHS_list = weakTieLocal(G, [[2, 7]], [[2, 7]], 0.2, score_dict, 3) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/ml_metrics.html b/docs/_modules/easygraph/ml_metrics.html deleted file mode 100644 index 57d232a3..00000000 --- a/docs/_modules/easygraph/ml_metrics.html +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - easygraph.ml_metrics — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.ml_metrics

-try:
-    from typing import Dict
-    from typing import List
-    from typing import Union
-
-    from easygraph._global import AUTHOR_EMAIL
-
-    from .base import BaseEvaluator
-    from .classification import VertexClassificationEvaluator
-    from .classification import available_classification_metrics
-    from .hypergraphs import HypergraphVertexClassificationEvaluator
-except:
-    print(
-        "Warning raise in module:ml_metrics. Please install Pytorch before you use"
-        " functions related to nueral network"
-    )
-
-
-
[docs]def build_evaluator( - task: str, - metric_configs: List[Union[str, Dict[str, dict]]], - validate_index: int = 0, -): - r"""Return the metric evaluator for the given task. - - Args: - ``task`` (``str``): The type of the task. The supported types include: ``graph_vertex_classification``, ``hypergraph_vertex_classification``, and ``user_item_recommender``. - ``metric_configs`` (``List[Union[str, Dict[str, dict]]]``): The list of metric names. - ``validate_index`` (``int``): The specified metric index used for validation. Defaults to ``0``. - """ - if task == "hypergraph_vertex_classification": - return HypergraphVertexClassificationEvaluator(metric_configs, validate_index) - else: - raise ValueError( - f"{task} is not supported yet. Please email '{AUTHOR_EMAIL}' to add it." - )
- - -# __all__ = [ -# "BaseEvaluator", -# "build_evaluator", -# "available_classification_metrics", -# "VertexClassificationEvaluator", -# "HypergraphVertexClassificationEvaluator", -# ] -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/ml_metrics/base.html b/docs/_modules/easygraph/ml_metrics/base.html deleted file mode 100644 index b3ac38de..00000000 --- a/docs/_modules/easygraph/ml_metrics/base.html +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - easygraph.ml_metrics.base — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.ml_metrics.base

-import abc
-
-from collections import defaultdict
-from functools import partial
-from typing import Dict
-from typing import List
-from typing import Union
-
-import torch
-
-from easygraph._global import AUTHOR_EMAIL
-
-
-
[docs]def format_metric_configs(task: str, metric_configs: List[Union[str, Dict[str, dict]]]): - r"""Format metric_configs. - - Args: - ``task`` (``str``): The type of the task. The supported types include: ``classification``, ``retrieval`` and ``recommender``. - ``metric_configs`` (``Dict[str, Dict[str, Union[str, int]]]``): The metric configs. - """ - task = task.lower() - if task == "classification": - import easygraph.ml_metrics.classification as module - - available_metrics = module.available_classification_metrics() - else: - raise ValueError( - f"Task {task} is not supported yet. Please email '{AUTHOR_EMAIL}' to" - " add it." - ) - metric_list = [] - for metric in metric_configs: - if isinstance(metric, str): - marker, func_name = metric, metric - assert func_name in available_metrics, ( - f"{func_name} is not supported yet. Please email '{AUTHOR_EMAIL}' to" - " add it." - ) - func = getattr(module, func_name) - elif isinstance(metric, dict): - assert len(metric) == 1 - func_name = list(metric.keys())[0] - assert func_name in available_metrics, ( - f"{func_name} is not supported yet. Please email '{AUTHOR_EMAIL}' to" - " add it." - ) - params = metric[func_name] - func = getattr(module, func_name) - func = partial(func, **params) - markder_list = [] - for k, v in params.items(): - _m = f"{k}@" - if isinstance(v, str): - _m += v - elif isinstance(v, int): - _m += str(v) - elif isinstance(v, float): - _m += f"{v:.4f}" - elif isinstance(v, list) or isinstance(v, tuple) or isinstance(v, set): - _m += "_".join([str(_v) for _v in v]) - else: - _m += str(v) - markder_list.append(_m) - marker = f"{func_name} -> {' | '.join(markder_list)}" - else: - raise ValueError - metric_list.append({"marker": marker, "func": func, "func_name": func_name}) - return metric_list
- - -
[docs]class BaseEvaluator: - r"""The base class for task-specified metric evaluators. - - Args: - ``task`` (``str``): The type of the task. The supported types include: ``classification``, ``retrieval`` and ``recommender``. - ``metric_configs`` (``List[Union[str, Dict[str, dict]]]``): The metric configurations. The key is the metric name and the value is the metric parameters. - ``validate_index`` (``int``): The specified metric index used for validation. Defaults to ``0``. - """ - - def __init__( - self, - task: str, - metric_configs: List[Union[str, Dict[str, dict]]], - validate_index: int = 0, - ): - self.validate_index = validate_index - metric_configs = format_metric_configs(task, metric_configs) - assert validate_index >= 0 and validate_index < len( - metric_configs - ), "The specified validate metric index is out of range." - self.marker_list, self.func_list = [], [] - for metric in metric_configs: - self.marker_list.append(metric["marker"]) - self.func_list.append(metric["func"]) - # init batch data containers - self.validate_res = [] - self.test_res_dict = defaultdict(list) - self.last_validate_res, self.last_test_res = None, {} - - @abc.abstractmethod - def __repr__(self) -> str: - r"""Print the Evaluator information.""" - -
[docs] def validate_add_batch( - self, batch_y_true: torch.Tensor, batch_y_pred: torch.Tensor - ): - import numpy as np - - r"""Add batch data for validation. - - Args: - ``batch_y_true`` (``torch.Tensor``): The ground truth data. Size :math:`(N_{batch}, -)`. - ``batch_y_pred`` (``torch.Tensor``): The predicted data. Size :math:`(N_{batch}, -)`. - """ - batch_res = self.func_list[self.validate_index]( - batch_y_true, batch_y_pred, ret_batch=True - ) - batch_res = np.array(batch_res) - if len(batch_res.shape) == 1: - batch_res = batch_res[:, np.newaxis] - self.validate_res.append(batch_res)
- -
[docs] def validate_epoch_res(self): - r"""For all added batch data, return the result of the evaluation on the specified ``validate_index``-th metric. - """ - import numpy as np - - if self.validate_res == [] and self.last_validate_res is not None: - return self.last_validate_res - assert self.validate_res != [], "No batch data added for validation." - self.last_validate_res = np.vstack(self.validate_res).mean(0).item() - # clear batch cache - self.validate_res = [] - return self.last_validate_res
- -
[docs] def test_add_batch(self, batch_y_true: torch.Tensor, batch_y_pred: torch.Tensor): - r"""Add batch data for testing. - - Args: - ``batch_y_true`` (``torch.Tensor``): The ground truth data. Size :math:`(N_{batch}, -)`. - ``batch_y_pred`` (``torch.Tensor``): The predicted data. Size :math:`(N_{batch}, -)`. - """ - import numpy as np - - for name, func in zip(self.marker_list, self.func_list): - batch_res = func(batch_y_true, batch_y_pred, ret_batch=True) - if not isinstance(batch_res, tuple): - batch_res = np.array(batch_res) - if len(batch_res.shape) == 1: - batch_res = batch_res[:, np.newaxis] - self.test_res_dict[name].append(batch_res) - else: - if self.test_res_dict[name] == []: - self.test_res_dict[name] = [list() for _ in range(len(batch_res))] - for idx, batch_sub_res in enumerate(batch_res): - batch_sub_res = np.array(batch_sub_res) - if len(batch_sub_res.shape) == 1: - batch_sub_res = batch_sub_res[:, np.newaxis] - self.test_res_dict[name][idx].append(batch_sub_res)
- -
[docs] def test_epoch_res(self): - r"""For all added batch data, return results of the evaluation on all the ml_metrics in ``metric_configs``. - """ - import numpy as np - - if self.test_res_dict == {} and self.last_test_res is not None: - return self.last_test_res - assert self.test_res_dict != {}, "No batch data added for testing." - for name, res_list in self.test_res_dict.items(): - if not isinstance(res_list[0], list): - self.last_test_res[name] = ( - np.vstack(res_list).mean(0).squeeze().tolist() - ) - else: - self.last_test_res[name] = [ - np.vstack(sub_res_list).mean(0).squeeze().tolist() - for sub_res_list in res_list - ] - # clear batch cache - self.test_res_dict = defaultdict(list) - return self.last_test_res
- -
[docs] def validate(self, y_true: torch.LongTensor, y_pred: torch.Tensor): - r"""Return the result of the evaluation on the specified ``validate_index``-th metric. - - Args: - ``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, -)`. - ``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, -)`. - """ - return self.func_list[self.validate_index](y_true, y_pred)
- -
[docs] def test(self, y_true: torch.LongTensor, y_pred: torch.Tensor): - r"""Return results of the evaluation on all the ml_metrics in ``metric_configs``. - - Args: - ``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, -)`. - ``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, -)`. - """ - return { - name: func(y_true, y_pred) - for name, func in zip(self.marker_list, self.func_list) - }
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/ml_metrics/classification.html b/docs/_modules/easygraph/ml_metrics/classification.html deleted file mode 100644 index 2f347626..00000000 --- a/docs/_modules/easygraph/ml_metrics/classification.html +++ /dev/null @@ -1,276 +0,0 @@ - - - - - - easygraph.ml_metrics.classification — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.ml_metrics.classification

-from typing import Dict
-from typing import List
-from typing import Union
-
-import sklearn.metrics as sm
-import torch
-
-from .base import BaseEvaluator
-
-
-__all__ = [
-    "available_classification_metrics",
-    "accuracy",
-    "f1_score",
-    "confusion_matrix",
-    "VertexClassificationEvaluator",
-]
-
-
-
[docs]def available_classification_metrics(): - r"""Return available ml_metrics for the classification task. - - The available ml_metrics are: ``accuracy``, ``f1_score``, ``confusion_matrix``. - """ - return ("accuracy", "f1_score", "confusion_matrix")
- - -def _format_inputs(y_true: torch.LongTensor, y_pred: torch.Tensor): - r"""Format the inputs. - - Args: - ``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, )`. - ``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, N_{class})` or :math:`(N_{samples}, )`. - """ - assert y_true.dim() == 1, "y_true must be 1D torch.LongTensor." - assert y_pred.dim() in (1, 2), "y_pred must be 1D or 2D torch.Tensor." - y_true = y_true.cpu().detach() - if y_pred.dim() == 2: - y_pred = y_pred.argmax(dim=1) - y_pred = y_pred.cpu().detach() - assert y_true.shape == y_pred.shape, "y_true and y_pred must have the same length." - return (y_true, y_pred) - - -
[docs]def accuracy(y_true: torch.LongTensor, y_pred: torch.Tensor): - r"""Calculate the accuracy score for the classification task. - - .. math:: - \text{Accuracy} = \frac{1}{N} \sum_{i=1}^{N} \mathcal{I}(y_i, \hat{y}_i), - - where :math:`\mathcal{I}(\cdot, \cdot)` is the indicator function, which is 1 if the two inputs are equal, and 0 otherwise. - :math:`y_i` and :math:`\hat{y}_i` are the ground truth and predicted labels for the i-th sample. - - Args: - ``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, )`. - ``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, N_{class})` or :math:`(N_{samples}, )`. - - Examples: - >>> import torch - >>> import easygraph.ml_metrics as dm - >>> y_true = torch.tensor([3, 2, 4]) - >>> y_pred = torch.tensor([ - [0.2, 0.3, 0.5, 0.4, 0.3], - [0.8, 0.2, 0.3, 0.5, 0.4], - [0.2, 0.4, 0.5, 0.2, 0.8], - ]) - >>> dm.classification.accuracy(y_true, y_pred) - 0.3333333432674408 - """ - y_true, y_pred = _format_inputs(y_true, y_pred) - return (y_true == y_pred).float().mean().item()
- - -
[docs]def f1_score(y_true: torch.LongTensor, y_pred: torch.Tensor, average: str = "macro"): - r"""Calculate the F1 score for the classification task. - - Args: - ``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, )`. - ``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, N_{class})` or :math:`(N_{samples}, )`. - ``average`` (``str``): The average method. Must be one of "macro", "micro", "weighted". - - Examples: - >>> import torch - >>> import easygraph.ml_metrics as dm - >>> y_true = torch.tensor([3, 2, 4, 0]) - >>> y_pred = torch.tensor([ - [0.2, 0.3, 0.5, 0.4, 0.3], - [0.8, 0.2, 0.3, 0.5, 0.4], - [0.2, 0.4, 0.5, 0.2, 0.8], - [0.8, 0.4, 0.5, 0.2, 0.8] - ]) - >>> dm.classification.f1_score(y_true, y_pred, "macro") - 0.41666666666666663 - >>> dm.classification.f1_score(y_true, y_pred, "micro") - 0.5 - >>> dm.classification.f1_score(y_true, y_pred, "weighted") - 0.41666666666666663 - """ - y_true, y_pred = _format_inputs(y_true, y_pred) - return sm.f1_score(y_true, y_pred, average=average)
- - -
[docs]def confusion_matrix(y_true: torch.LongTensor, y_pred: torch.Tensor): - r"""Calculate the confusion matrix for the classification task. - - Args: - ``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, )`. - ``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, N_{class})` or :math:`(N_{samples}, )`. - - Examples: - >>> import torch - >>> import easygraph.ml_metrics as dm - >>> y_true = torch.tensor([3, 2, 4, 0]) - >>> y_pred = torch.tensor([ - [0.2, 0.3, 0.5, 0.4, 0.3], - [0.8, 0.2, 0.3, 0.5, 0.4], - [0.2, 0.4, 0.5, 0.2, 0.8], - [0.8, 0.4, 0.5, 0.2, 0.8] - ]) - >>> dm.classification.confusion_matrix(y_true, y_pred) - array([[1, 0, 0, 0], - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 0, 1]]) - """ - y_true, y_pred = _format_inputs(y_true, y_pred) - return sm.confusion_matrix(y_true, y_pred)
- - -# Vertex Classification Evaluator -
[docs]class VertexClassificationEvaluator(BaseEvaluator): - r"""Return the metric evaluator for vertex classification task. The supported ml_metrics includes: ``accuracy``, ``f1_score``, ``confusion_matrix``. - - Args: - ``metric_configs`` (``List[Union[str, Dict[str, dict]]]``): The metric configurations. The key is the metric name and the value is the metric parameters. - ``validate_index`` (``int``): The specified metric index used for validation. Defaults to ``0``. - """ - - def __init__( - self, - metric_configs: List[Union[str, Dict[str, dict]]], - validate_index: int = 0, - ): - super().__init__("classification", metric_configs, validate_index) - -
[docs] def validate(self, y_true: torch.LongTensor, y_pred: torch.Tensor): - r"""Return the result of the evaluation on the specified ``validate_index``-th metric. - - Args: - ``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, )`. - ``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, N_{class})` or :math:`(N_{samples}, )`. - """ - return super().validate(y_true, y_pred)
- -
[docs] def test(self, y_true: torch.LongTensor, y_pred: torch.Tensor): - r"""Return results of the evaluation on all the ml_metrics in ``metric_configs``. - - Args: - ``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, )`. - ``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, N_{class})` or :math:`(N_{samples}, )`. - """ - return super().test(y_true, y_pred)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/ml_metrics/hypergraphs/hypergraph.html b/docs/_modules/easygraph/ml_metrics/hypergraphs/hypergraph.html deleted file mode 100644 index 4ed89b0e..00000000 --- a/docs/_modules/easygraph/ml_metrics/hypergraphs/hypergraph.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - easygraph.ml_metrics.hypergraphs.hypergraph — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.ml_metrics.hypergraphs.hypergraph

-from typing import Dict
-from typing import List
-from typing import Union
-
-import torch
-
-from ..classification import VertexClassificationEvaluator
-
-
-
[docs]class HypergraphVertexClassificationEvaluator(VertexClassificationEvaluator): - r"""Return the metric evaluator for vertex classification task on the hypergraph structure. The supported ml_metrics includes: ``accuracy``, ``f1_score``, ``confusion_matrix``. - - Args: - ``metric_configs`` (``List[Union[str, Dict[str, dict]]]``): The metric configurations. The key is the metric name and the value is the metric parameters. - ``validate_index`` (``int``): The specified metric index used for validation. Defaults to ``0``. - - Examples: - >>> import torch - >>> import easygraph.ml_metrics as dm - >>> evaluator = dm.HypergraphVertexClassificationEvaluator( - [ - "accuracy", - {"f1_score": {"average": "macro"}}, - ], - 0 - ) - >>> y_true = torch.tensor([0, 0, 1, 1, 2, 2]) - >>> y_pred = torch.tensor([0, 2, 1, 2, 1, 2]) - >>> evaluator.validate(y_true, y_pred) - 0.5 - >>> evaluator.test(y_true, y_pred) - { - 'accuracy': 0.5, - 'f1_score -> average@macro': 0.5222222222222221 - } - """ - - def __init__( - self, metric_configs: List[Union[str, Dict[str, dict]]], validate_index: int = 0 - ): - super().__init__(metric_configs, validate_index) - -
[docs] def validate(self, y_true: torch.LongTensor, y_pred: torch.Tensor): - return super().validate(y_true, y_pred)
- -
[docs] def test(self, y_true: torch.LongTensor, y_pred: torch.Tensor): - return super().test(y_true, y_pred)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/dhcf.html b/docs/_modules/easygraph/model/hypergraphs/dhcf.html deleted file mode 100644 index b3a018e6..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/dhcf.html +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - easygraph.model.hypergraphs.dhcf — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.dhcf

-from typing import Tuple
-
-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-
-from easygraph.classes import Hypergraph
-
-
-
[docs]class DHCF(nn.Module): - r"""The DHCF model proposed in `Dual Channel Hypergraph Collaborative Filtering <https://dl.acm.org/doi/10.1145/3394486.3403253>`_ paper (KDD 2020). - - .. note:: - - The user and item embeddings and trainable parameters are initialized with xavier_uniform distribution. - - Args: - ``num_users`` (``int``): The Number of users. - ``num_items`` (``int``): The Number of items. - ``emb_dim`` (``int``): Embedding dimension. - ``num_layers`` (``int``): The Number of layers. Defaults to ``3``. - ``drop_rate`` (``float``): The dropout probability. Defaults to ``0.5``. - """ - - def __init__( - self, - num_users: int, - num_items: int, - emb_dim: int, - num_layers: int = 3, - drop_rate: float = 0.5, - ) -> None: - super().__init__() - self.num_users, self.num_items = num_users, num_items - self.num_layers = num_layers - self.drop_rate = drop_rate - self.u_embedding = nn.Embedding(num_users, emb_dim) - self.i_embedding = nn.Embedding(num_items, emb_dim) - self.W_gc, self.W_bi = nn.ModuleList(), nn.ModuleList() - for _ in range(self.num_layers): - self.W_gc.append(nn.Linear(emb_dim, emb_dim)) - self.W_bi.append(nn.Linear(emb_dim, emb_dim)) - self.reset_parameters() - -
[docs] def reset_parameters(self): - r"""Initialize learnable parameters.""" - nn.init.xavier_uniform_(self.u_embedding.weight) - nn.init.xavier_uniform_(self.i_embedding.weight) - for W_gc, W_bi in zip(self.W_gc, self.W_bi): - nn.init.xavier_uniform_(W_gc.weight) - nn.init.xavier_uniform_(W_bi.weight) - nn.init.constant_(W_gc.bias, 0) - nn.init.constant_(W_bi.bias, 0)
- -
[docs] def forward( - self, hg_ui: Hypergraph, hg_iu: Hypergraph - ) -> Tuple[torch.Tensor, torch.Tensor]: - r"""The forward function. - - Args: - ``hg_ui`` (``eg.Hypergraph``): The hypergraph structure that users as vertices. - ``hg_iu`` (``eg.Hypergraph``): The hypergraph structure that items as vertices. - """ - u_embs = self.u_embedding.weight - i_embs = self.i_embedding.weight - all_embs = torch.cat([u_embs, i_embs], dim=0) - - embs_list = [all_embs] - for _idx in range(self.num_layers): - u_embs, i_embs = torch.split( - all_embs, [self.num_users, self.num_items], dim=0 - ) - # ========================================================================================== - # Two JHConv Layers for users and items, respectively. - u_embs = hg_ui.smoothing_with_HGNN(u_embs) - i_embs = hg_iu.smoothing_with_HGNN(i_embs) - g_embs = torch.cat([u_embs, i_embs], dim=0) - sum_embs = F.leaky_relu( - self.W_gc[_idx](g_embs) + g_embs, negative_slope=0.2 - ) - # ========================================================================================== - - bi_embs = all_embs * g_embs - bi_embs = F.leaky_relu(self.W_bi[_idx](bi_embs), negative_slope=0.2) - - all_embs = sum_embs + bi_embs - all_embs = F.dropout(all_embs, p=self.drop_rate, training=self.training) - all_embs = F.normalize(all_embs, p=2, dim=1) - - embs_list.append(all_embs) - embs = torch.stack(embs_list, dim=1) - embs = torch.mean(embs, dim=1) - - u_embs, i_embs = torch.split(embs, [self.num_users, self.num_items], dim=0) - return u_embs, i_embs
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/dhne.html b/docs/_modules/easygraph/model/hypergraphs/dhne.html deleted file mode 100644 index ce2feb87..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/dhne.html +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - - easygraph.model.hypergraphs.dhne — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.dhne

-import torch
-import torch.nn as nn
-
-
-
[docs]class DHNE(nn.Module): - r"""The DHNE model proposed in `Structural Deep Embedding for Hyper-Networks <https://arxiv.org/abs/1711.10146>`_ paper (AAAI 2018). - - Parameters: - ``dim_feature`` (``int``): : feature dimension list ( len = 3) - ``embedding_size`` (``int``): :The embedding dimension size - ``hidden_size`` (``int``): The hidden full connected layer size. - - """ - - def __init__(self, dim_feature, embedding_size, hidden_size): - super(DHNE, self).__init__() - self.dim_feature = dim_feature - self.embedding_size = embedding_size - self.hidden_size = hidden_size - self.encode0 = nn.Sequential( - nn.Linear( - in_features=self.dim_feature[0], out_features=self.embedding_size[0] - ) - ) - self.encode1 = nn.Sequential( - nn.Linear( - in_features=self.dim_feature[1], out_features=self.embedding_size[1] - ) - ) - self.encode2 = nn.Sequential( - nn.Linear( - in_features=self.dim_feature[2], out_features=self.embedding_size[2] - ) - ) - self.decode_layer0 = nn.Linear( - in_features=self.embedding_size[0], out_features=self.dim_feature[0] - ) - self.decode_layer1 = nn.Linear( - in_features=self.embedding_size[1], out_features=self.dim_feature[1] - ) - self.decode_layer2 = nn.Linear( - in_features=self.embedding_size[2], out_features=self.dim_feature[2] - ) - - self.hidden_layer = nn.Linear( - in_features=sum(self.embedding_size), out_features=self.hidden_size - ) - self.ouput_layer = nn.Linear(in_features=self.hidden_size, out_features=1) - -
[docs] def forward(self, input0, input1, input2): - input0 = self.encode0(input0) - input0 = torch.tanh(input0) - decode0 = self.decode_layer0(input0) - decode0 = torch.sigmoid(decode0) - - input1 = self.encode1(input1) - input1 = torch.tanh(input1) - decode1 = self.decode_layer1(input1) - decode1 = torch.sigmoid(decode1) - - input2 = self.encode2(input2) - input2 = torch.tanh(input2) - decode2 = self.decode_layer2(input2) - decode2 = torch.sigmoid(decode2) - - merged = torch.tanh(torch.cat((input0, input1, input2), dim=1)) - merged = self.hidden_layer(merged) - merged = self.ouput_layer(merged) - merged = torch.sigmoid(merged) - return [decode0, decode1, decode2, merged]
-
- -
-
- -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/hgnn.html b/docs/_modules/easygraph/model/hypergraphs/hgnn.html deleted file mode 100644 index d23dd72b..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/hgnn.html +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - easygraph.model.hypergraphs.hgnn — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.hgnn

-import torch
-import torch.nn as nn
-
-from easygraph.nn import HGNNConv
-
-
-
[docs]class HGNN(nn.Module): - r"""The HGNN model proposed in `Hypergraph Neural Networks <https://arxiv.org/pdf/1809.09401>`_ paper (AAAI 2019). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``use_bn`` (``bool``): If set to ``True``, use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``, optional): Dropout ratio. Defaults to 0.5. - """ - - def __init__( - self, - in_channels: int, - hid_channels: int, - num_classes: int, - use_bn: bool = False, - drop_rate: float = 0.5, - ) -> None: - super().__init__() - self.layers = nn.ModuleList() - self.layers.append( - HGNNConv(in_channels, hid_channels, use_bn=use_bn, drop_rate=drop_rate) - ) - self.layers.append( - HGNNConv(hid_channels, num_classes, use_bn=use_bn, is_last=True) - ) - -
[docs] def forward(self, X: torch.Tensor, hg: "Hypergraph") -> torch.Tensor: - r"""The forward function. - - Args: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - for layer in self.layers: - X = layer(X, hg) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/hgnnp.html b/docs/_modules/easygraph/model/hypergraphs/hgnnp.html deleted file mode 100644 index 53b2ed3e..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/hgnnp.html +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - easygraph.model.hypergraphs.hgnnp — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.hgnnp

-import torch
-import torch.nn as nn
-
-from easygraph.nn import HGNNPConv
-
-
-
[docs]class HGNNP(nn.Module): - r"""The HGNN :sup:`+` model proposed in `HGNN+: General Hypergraph Neural Networks <https://ieeexplore.ieee.org/document/9795251>`_ paper (IEEE T-PAMI 2022). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``use_bn`` (``bool``): If set to ``True``, use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``, optional): Dropout ratio. Defaults to ``0.5``. - """ - - def __init__( - self, - in_channels: int, - hid_channels: int, - num_classes: int, - use_bn: bool = False, - drop_rate: float = 0.5, - ) -> None: - super().__init__() - self.layers = nn.ModuleList() - self.layers.append( - HGNNPConv(in_channels, hid_channels, use_bn=use_bn, drop_rate=drop_rate) - ) - self.layers.append( - HGNNPConv(hid_channels, num_classes, use_bn=use_bn, is_last=True) - ) - -
[docs] def forward(self, X: torch.Tensor, hg: "eg.Hypergraph") -> torch.Tensor: - r"""The forward function. - - Args: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - for layer in self.layers: - X = layer(X, hg) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/hnhn.html b/docs/_modules/easygraph/model/hypergraphs/hnhn.html deleted file mode 100644 index 23bc4861..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/hnhn.html +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - easygraph.model.hypergraphs.hnhn — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.hnhn

-import torch
-import torch.nn as nn
-
-from easygraph.nn import HNHNConv
-
-
-
[docs]class HNHN(nn.Module): - r"""The HNHN model proposed in `HNHN: Hypergraph Networks with Hyperedge Neurons <https://arxiv.org/pdf/2006.12278.pdf>`_ paper (ICML 2020). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``use_bn`` (``bool``): If set to ``True``, use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``, optional): Dropout ratio. Defaults to ``0.5``. - """ - - def __init__( - self, - in_channels: int, - hid_channels: int, - num_classes: int, - use_bn: bool = False, - drop_rate: float = 0.5, - ) -> None: - super().__init__() - self.layers = nn.ModuleList() - self.layers.append( - HNHNConv(in_channels, hid_channels, use_bn=use_bn, drop_rate=drop_rate) - ) - self.layers.append( - HNHNConv(hid_channels, num_classes, use_bn=use_bn, is_last=True) - ) - -
[docs] def forward(self, X: torch.Tensor, hg: "eg.Hypergraph") -> torch.Tensor: - r"""The forward function. - - Args: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - for layer in self.layers: - X = layer(X, hg) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/hwnn.html b/docs/_modules/easygraph/model/hypergraphs/hwnn.html deleted file mode 100644 index 03586c81..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/hwnn.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - easygraph.model.hypergraphs.hwnn — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.hwnn

-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-
-from easygraph.nn import HWNNConv
-
-
-
[docs]class HWNN(nn.Module): - r"""The HGNN model proposed in `Hypergraph Neural Networks <https://arxiv.org/pdf/1809.09401>`_ paper (AAAI 2019). - - Parameters: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``ncount`` (``int``): The Number of node in the hypergraph. - ``hyper_snapshot_num`` (``int``): The Number of snapshots splited from hypergraph. - ``drop_rate`` (``float``, optional): Dropout ratio. Defaults to 0.5. - """ - - def __init__( - self, - in_channels: int, - num_classes: int, - ncount: int, - hyper_snapshot_num: int = 1, - hid_channels: int = 128, - drop_rate: float = 0.01, - ) -> None: - super().__init__() - self.drop_rate = drop_rate - self.convolution_1 = HWNNConv( - in_channels, hid_channels, ncount, K1=3, K2=3, approx=True - ) - self.convolution_2 = HWNNConv( - hid_channels, num_classes, ncount, K1=3, K2=3, approx=True - ) - self.par = torch.nn.Parameter(torch.Tensor(hyper_snapshot_num)) - torch.nn.init.uniform_(self.par, 0, 0.99) - -
[docs] def forward(self, X: torch.Tensor, hgs: list) -> torch.Tensor: - r"""The forward function. - Parameters: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - ``hgs`` (``list`` of ``Hypergraph``): A list of hypergraph structures whcih stands for snapshots. - """ - channel = [] - hyper_snapshot_num = len(hgs) - for snap_index in range(hyper_snapshot_num): - hg = hgs[snap_index] - Y = F.relu(self.convolution_1(X, hg)) - Y = F.dropout(Y, self.drop_rate) - Y = self.convolution_2(Y, hg) - Y = F.log_softmax(Y, dim=1) - channel.append(Y) - X = torch.zeros_like(channel[0]) - for ind in range(hyper_snapshot_num): - X = X + self.par[ind] * channel[ind] - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/hypergcn.html b/docs/_modules/easygraph/model/hypergraphs/hypergcn.html deleted file mode 100644 index 2b7753f2..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/hypergcn.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - easygraph.model.hypergraphs.hypergcn — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.hypergcn

-import torch
-import torch.nn as nn
-
-from easygraph.classes import Hypergraph
-from easygraph.nn import HyperGCNConv
-
-
-
[docs]class HyperGCN(nn.Module): - r"""The HyperGCN model proposed in `HyperGCN: A New Method of Training Graph Convolutional Networks on Hypergraphs <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper (NeurIPS 2019). - - Parameters: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``use_mediator`` (``str``): Whether to use mediator to transform the hyperedges to edges in the graph. Defaults to ``False``. - ``fast`` (``bool``): If set to ``True``, the transformed graph structure will be computed once from the input hypergraph and vertex features, and cached for future use. Defaults to ``True``. - ``drop_rate`` (``float``, optional): Dropout ratio. Defaults to 0.5. - """ - - def __init__( - self, - in_channels: int, - hid_channels: int, - num_classes: int, - use_mediator: bool = False, - use_bn: bool = False, - fast: bool = True, - drop_rate: float = 0.5, - ) -> None: - super().__init__() - self.fast = fast - self.cached_g = None - self.with_mediator = use_mediator - self.layers = nn.ModuleList() - self.layers.append( - HyperGCNConv( - in_channels, - hid_channels, - use_mediator, - use_bn=use_bn, - drop_rate=drop_rate, - ) - ) - self.layers.append( - HyperGCNConv( - hid_channels, num_classes, use_mediator, use_bn=use_bn, is_last=True - ) - ) - -
[docs] def forward(self, X: torch.Tensor, hg: "eg.Hypergraph") -> torch.Tensor: - r"""The forward function. - - Parameters: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - if self.fast: - if self.cached_g is None: - self.cached_g = Hypergraph.from_hypergraph_hypergcn( - hg, X, self.with_mediator - ) - for layer in self.layers: - X = layer(X, hg, self.cached_g) - else: - for layer in self.layers: - X = layer(X, hg) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/setgnn.html b/docs/_modules/easygraph/model/hypergraphs/setgnn.html deleted file mode 100644 index fec8284d..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/setgnn.html +++ /dev/null @@ -1,410 +0,0 @@ - - - - - - - - easygraph.model.hypergraphs.setgnn — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.setgnn

-from collections import Counter
-
-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-
-from easygraph.nn.convs.common import MLP
-from easygraph.nn.convs.hypergraphs.halfnlh_conv import HalfNLHconv
-from torch.nn import Linear
-
-
-__all__ = ["SetGNN"]
-
-
-
[docs]class SetGNN(nn.Module): - r"""The SetGNN model proposed in `YOU ARE ALLSET: A MULTISET LEARNING FRAMEWORK FOR HYPERGRAPH NEURAL NETWORKS <https://openreview.net/pdf?id=hpBTIv2uy_E>`_ paper (ICLR 2022). - - Parameters: - ``num_features`` (``int``): : The dimension of node features. - ``num_classes`` (``int``): The Number of class of the classification task. - ``Classifier_hidden`` (``int``): Decoder hidden units. - ``Classifier_num_layers`` (``int``): Layers of decoder. - ``MLP_hidden`` (``int``): Encoder hidden units. - ``MLP_num_layers`` (``int``): Layers of encoder. - ``dropout`` (``float``, optional): Dropout ratio. Defaults to 0.5. - ``aggregate`` (``str``): The aggregation method. Defaults to ``add`` - ``normalization`` (``str``): The normalization method. Defaults to ``ln`` - ``deepset_input_norm`` (``bool``): Defaults to True. - ``heads`` (``int``): Defaults to 1 - `PMA`` (``bool``): Defaults to True - `GPR`` (``bool``): Defaults to False - `LearnMask`` (``bool``): Defaults to False - `norm`` (``Tensor``): The weight for edges in bipartite graphs, correspond to data.edge_index - - """ - - def __init__( - self, - num_features, - num_classes, - Classifier_hidden=64, - Classifier_num_layers=2, - MLP_hidden=64, - MLP_num_layers=2, - All_num_layers=2, - dropout=0.5, - aggregate="mean", - normalization="ln", - deepset_input_norm=True, - heads=1, - PMA=True, - GPR=False, - LearnMask=False, - norm=None, - self_loop=True, - ): - super(SetGNN, self).__init__() - """ - args should contain the following: - V_in_dim, V_enc_hid_dim, V_dec_hid_dim, V_out_dim, V_enc_num_layers, V_dec_num_layers - E_in_dim, E_enc_hid_dim, E_dec_hid_dim, E_out_dim, E_enc_num_layers, E_dec_num_layers - All_num_layers,dropout - !!! V_in_dim should be the dimension of node features - !!! E_out_dim should be the number of classes (for classification) - """ - - # Now set all dropout the same, but can be different - self.All_num_layers = All_num_layers - self.dropout = dropout - self.aggr = aggregate - self.NormLayer = normalization - self.InputNorm = deepset_input_norm - self.GPR = GPR - self.LearnMask = LearnMask - # Now define V2EConvs[i], V2EConvs[i] for ith layers - # Currently we assume there's no hyperedge features, which means V_out_dim = E_in_dim - # If there's hyperedge features, concat with Vpart decoder output features [V_feat||E_feat] - self.V2EConvs = nn.ModuleList() - self.E2VConvs = nn.ModuleList() - self.bnV2Es = nn.ModuleList() - self.bnE2Vs = nn.ModuleList() - self.edge_index = None - self.self_loop = self_loop - if self.LearnMask: - self.Importance = nn.Parameter(torch.ones(norm.size())) - - if self.All_num_layers == 0: - self.classifier = MLP( - in_channels=num_features, - hidden_channels=Classifier_hidden, - out_channels=num_classes, - num_layers=Classifier_num_layers, - dropout=self.dropout, - normalization=self.NormLayer, - InputNorm=False, - ) - else: - self.V2EConvs.append( - HalfNLHconv( - in_dim=num_features, - hid_dim=MLP_hidden, - out_dim=MLP_hidden, - num_layers=MLP_num_layers, - dropout=self.dropout, - normalization=self.NormLayer, - InputNorm=self.InputNorm, - heads=heads, - attention=PMA, - ) - ) - self.bnV2Es.append(nn.BatchNorm1d(MLP_hidden)) - self.E2VConvs.append( - HalfNLHconv( - in_dim=MLP_hidden, - hid_dim=MLP_hidden, - out_dim=MLP_hidden, - num_layers=MLP_num_layers, - dropout=self.dropout, - normalization=self.NormLayer, - InputNorm=self.InputNorm, - heads=heads, - attention=PMA, - ) - ) - self.bnE2Vs.append(nn.BatchNorm1d(MLP_hidden)) - for _ in range(self.All_num_layers - 1): - self.V2EConvs.append( - HalfNLHconv( - in_dim=MLP_hidden, - hid_dim=MLP_hidden, - out_dim=MLP_hidden, - num_layers=MLP_num_layers, - dropout=self.dropout, - normalization=self.NormLayer, - InputNorm=self.InputNorm, - heads=heads, - attention=PMA, - ) - ) - self.bnV2Es.append(nn.BatchNorm1d(MLP_hidden)) - self.E2VConvs.append( - HalfNLHconv( - in_dim=MLP_hidden, - hid_dim=MLP_hidden, - out_dim=MLP_hidden, - num_layers=MLP_num_layers, - dropout=self.dropout, - normalization=self.NormLayer, - InputNorm=self.InputNorm, - heads=heads, - attention=PMA, - ) - ) - self.bnE2Vs.append(nn.BatchNorm1d(MLP_hidden)) - - if self.GPR: - self.MLP = MLP( - in_channels=num_features, - hidden_channels=MLP_hidden, - out_channels=MLP_hidden, - num_layers=MLP_num_layers, - dropout=self.dropout, - normalization=self.NormLayer, - InputNorm=False, - ) - self.GPRweights = Linear(self.All_num_layers + 1, 1, bias=False) - self.classifier = MLP( - in_channels=MLP_hidden, - hidden_channels=Classifier_hidden, - out_channels=num_classes, - num_layers=Classifier_num_layers, - dropout=self.dropout, - normalization=self.NormLayer, - InputNorm=False, - ) - else: - self.classifier = MLP( - in_channels=MLP_hidden, - hidden_channels=Classifier_hidden, - out_channels=num_classes, - num_layers=Classifier_num_layers, - dropout=self.dropout, - normalization=self.NormLayer, - InputNorm=False, - ) - -
[docs] def generate_edge_index(self, dataset, self_loop=False): - edge_list = dataset["edge_list"] - e_ind = 0 - edge_index = [[], []] - for e in edge_list: - for n in e: - edge_index[0].append(n) - edge_index[1].append(e_ind) - e_ind += 1 - edge_index = torch.tensor(edge_index).type(torch.LongTensor) - if self_loop: - hyperedge_appear_fre = Counter(edge_index[1].numpy()) - skip_node_lst = [] - for edge in hyperedge_appear_fre: - if hyperedge_appear_fre[edge] == 1: - skip_node = edge_index[0][torch.where(edge_index[1] == edge)[0]] - skip_node_lst.append(skip_node) - num_nodes = dataset["num_vertices"] - new_edge_idx = len(edge_index[1]) + 1 - new_edges = torch.zeros( - (2, num_nodes - len(skip_node_lst)), dtype=edge_index.dtype - ) - tmp_count = 0 - for i in range(num_nodes): - if i not in skip_node_lst: - new_edges[0][tmp_count] = i - new_edges[1][tmp_count] = new_edge_idx - new_edge_idx += 1 - tmp_count += 1 - - edge_index = torch.Tensor(edge_index).type(torch.LongTensor) - edge_index = torch.cat((edge_index, new_edges), dim=1) - _, sorted_idx = torch.sort(edge_index[0]) - edge_index = torch.Tensor(edge_index[:, sorted_idx]).type(torch.LongTensor) - - return edge_index
- -
[docs] def reset_parameters(self): - for layer in self.V2EConvs: - layer.reset_parameters() - for layer in self.E2VConvs: - layer.reset_parameters() - for layer in self.bnV2Es: - layer.reset_parameters() - for layer in self.bnE2Vs: - layer.reset_parameters() - self.classifier.reset_parameters() - if self.GPR: - self.MLP.reset_parameters() - self.GPRweights.reset_parameters() - if self.LearnMask: - nn.init.ones_(self.Importance)
- -
[docs] def forward(self, data): - """ - The data should contain the follows - data.x: node features - data.edge_index: edge list (of size (2,|E|)) where data.edge_index[0] contains nodes and data.edge_index[1] contains hyperedges - !!! Note that self loop should be assigned to a new (hyper)edge id!!! - !!! Also note that the (hyper)edge id should start at 0 (akin to node id) - data.norm: The weight for edges in bipartite graphs, correspond to data.edge_index - !!! Note that we output final node representation. Loss should be defined outside. - """ - if self.edge_index is None: - self.edge_index = self.generate_edge_index(data, self.self_loop) - # print("generate_edge_index:", self.edge_index.shape) - x, edge_index = data["features"], self.edge_index - if data["weight"] == None: - norm = torch.ones(edge_index.size()[1]) - else: - norm = data["weight"] - - if self.LearnMask: - norm = self.Importance * norm - - reversed_edge_index = torch.stack([edge_index[1], edge_index[0]], dim=0) - if self.GPR: - xs = [] - xs.append(F.relu(self.MLP(x))) - for i, _ in enumerate(self.V2EConvs): - x = F.relu(self.V2EConvs[i](x, edge_index, norm, self.aggr)) - # x = self.bnV2Es[i](x) - x = F.dropout(x, p=self.dropout, training=self.training) - x = self.E2VConvs[i](x, reversed_edge_index, norm, self.aggr) - x = F.relu(x) - xs.append(x) - # x = self.bnE2Vs[i](x) - x = F.dropout(x, p=self.dropout, training=self.training) - x = torch.stack(xs, dim=-1) - x = self.GPRweights(x).squeeze() - x = self.classifier(x) - else: - x = F.dropout(x, p=0.2, training=self.training) # Input dropout - for i, _ in enumerate(self.V2EConvs): - x = F.relu(self.V2EConvs[i](x, edge_index, norm, self.aggr)) - # x = self.bnV2Es[i](x) - x = F.dropout(x, p=self.dropout, training=self.training) - x = F.relu(self.E2VConvs[i](x, reversed_edge_index, norm, self.aggr)) - # x = self.bnE2Vs[i](x) - x = F.dropout(x, p=self.dropout, training=self.training) - x = self.classifier(x) - - return x
-
- -
-
- -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/model/hypergraphs/unignn.html b/docs/_modules/easygraph/model/hypergraphs/unignn.html deleted file mode 100644 index 3e85136b..00000000 --- a/docs/_modules/easygraph/model/hypergraphs/unignn.html +++ /dev/null @@ -1,328 +0,0 @@ - - - - - - easygraph.model.hypergraphs.unignn — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.model.hypergraphs.unignn

-import torch
-import torch.nn as nn
-
-from easygraph.nn import MultiHeadWrapper
-from easygraph.nn import UniGATConv
-from easygraph.nn import UniGCNConv
-from easygraph.nn import UniGINConv
-from easygraph.nn import UniSAGEConv
-
-
-__all__ = [
-    "UniGCN",
-    "UniGAT",
-    "UniSAGE",
-    "UniGIN",
-]
-
-
-
[docs]class UniGCN(nn.Module): - r"""The UniGCN model proposed in `UniGNN: a Unified Framework for Graph and Hypergraph Neural Networks <https://arxiv.org/pdf/2105.00956.pdf>`_ paper (IJCAI 2021). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``use_bn`` (``bool``): If set to ``True``, use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``, optional): Dropout ratio. Defaults to ``0.5``. - """ - - def __init__( - self, - in_channels: int, - hid_channels: int, - num_classes: int, - use_bn: bool = False, - drop_rate: float = 0.5, - ) -> None: - super().__init__() - self.layers = nn.ModuleList() - self.layers.append( - UniGCNConv(in_channels, hid_channels, use_bn=use_bn, drop_rate=drop_rate) - ) - self.layers.append( - UniGCNConv(hid_channels, num_classes, use_bn=use_bn, is_last=True) - ) - -
[docs] def forward(self, X: torch.Tensor, hg: "eg.Hypergraph") -> torch.Tensor: - r"""The forward function. - - Args: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - for layer in self.layers: - X = layer(X, hg) - return X
- - -
[docs]class UniGAT(nn.Module): - r"""The UniGAT model proposed in `UniGNN: a Unified Framework for Graph and Hypergraph Neural Networks <https://arxiv.org/pdf/2105.00956.pdf>`_ paper (IJCAI 2021). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``num_heads`` (``int``): The Number of attention head in each layer. - ``use_bn`` (``bool``): If set to ``True``, use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): The dropout probability. Defaults to ``0.5``. - ``atten_neg_slope`` (``float``): Hyper-parameter of the ``LeakyReLU`` activation of edge attention. Defaults to 0.2. - """ - - def __init__( - self, - in_channels: int, - hid_channels: int, - num_classes: int, - num_heads: int, - use_bn: bool = False, - drop_rate: float = 0.5, - atten_neg_slope: float = 0.2, - ) -> None: - super().__init__() - self.drop_layer = nn.Dropout(drop_rate) - self.multi_head_layer = MultiHeadWrapper( - num_heads, - "concat", - UniGATConv, - in_channels=in_channels, - out_channels=hid_channels, - use_bn=use_bn, - drop_rate=drop_rate, - atten_neg_slope=atten_neg_slope, - ) - # The original implementation has applied activation layer after the final layer. - # Thus, we donot set ``is_last`` to ``True``. - self.out_layer = UniGATConv( - hid_channels * num_heads, - num_classes, - use_bn=use_bn, - drop_rate=drop_rate, - atten_neg_slope=atten_neg_slope, - is_last=False, - ) - -
[docs] def forward(self, X: torch.Tensor, hg: "eg.Hypergraph") -> torch.Tensor: - r"""The forward function. - - Args: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - X = self.drop_layer(X) - X = self.multi_head_layer(X=X, hg=hg) - X = self.drop_layer(X) - X = self.out_layer(X, hg) - return X
- - -
[docs]class UniSAGE(nn.Module): - r"""The UniSAGE model proposed in `UniGNN: a Unified Framework for Graph and Hypergraph Neural Networks <https://arxiv.org/pdf/2105.00956.pdf>`_ paper (IJCAI 2021). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``use_bn`` (``bool``): If set to ``True``, use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``, optional): Dropout ratio. Defaults to ``0.5``. - """ - - def __init__( - self, - in_channels: int, - hid_channels: int, - num_classes: int, - use_bn: bool = False, - drop_rate: float = 0.5, - ) -> None: - super().__init__() - self.layers = nn.ModuleList() - self.layers.append( - UniSAGEConv(in_channels, hid_channels, use_bn=use_bn, drop_rate=drop_rate) - ) - self.layers.append( - UniSAGEConv(hid_channels, num_classes, use_bn=use_bn, is_last=True) - ) - -
[docs] def forward(self, X: torch.Tensor, hg: "eg.Hypergraph") -> torch.Tensor: - r"""The forward function. - - Args: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - for layer in self.layers: - X = layer(X, hg) - return X
- - -
[docs]class UniGIN(nn.Module): - r"""The UniGIN model proposed in `UniGNN: a Unified Framework for Graph and Hypergraph Neural Networks <https://arxiv.org/pdf/2105.00956.pdf>`_ paper (IJCAI 2021). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``hid_channels`` (``int``): :math:`C_{hid}` is the number of hidden channels. - ``num_classes`` (``int``): The Number of class of the classification task. - ``eps`` (``float``): The epsilon value. Defaults to ``0.0``. - ``train_eps`` (``bool``): If set to ``True``, the epsilon value will be trainable. Defaults to ``False``. - ``use_bn`` (``bool``): If set to ``True``, use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``, optional): Dropout ratio. Defaults to ``0.5``. - """ - - def __init__( - self, - in_channels: int, - hid_channels: int, - num_classes: int, - eps: float = 0.0, - train_eps: bool = False, - use_bn: bool = False, - drop_rate: float = 0.5, - ) -> None: - super().__init__() - self.layers = nn.ModuleList() - self.layers.append( - UniGINConv( - in_channels, - hid_channels, - eps=eps, - train_eps=train_eps, - use_bn=use_bn, - drop_rate=drop_rate, - ) - ) - self.layers.append( - UniGINConv( - hid_channels, - num_classes, - eps=eps, - train_eps=train_eps, - use_bn=use_bn, - is_last=True, - ) - ) - -
[docs] def forward(self, X: torch.Tensor, hg: "eg.Hypergraph") -> torch.Tensor: - r"""The forward function. - - Args: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - for layer in self.layers: - X = layer(X, hg) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/common.html b/docs/_modules/easygraph/nn/convs/common.html deleted file mode 100644 index b5fdb8eb..00000000 --- a/docs/_modules/easygraph/nn/convs/common.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - easygraph.nn.convs.common — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.nn.convs.common

-import torch
-import torch.nn as nn
-
-
-
[docs]class MultiHeadWrapper(nn.Module): - r"""A wrapper to apply multiple heads to a given layer. - - Args: - ``num_heads`` (``int``): The number of heads. - ``readout`` (``bool``): The readout method. Can be ``"mean"``, ``"max"``, ``"sum"``, or ``"concat"``. - ``layer`` (``nn.Module``): The layer to apply multiple heads. - ``**kwargs``: The keyword arguments for the layer. - - """ - - def __init__( - self, num_heads: int, readout: str, layer: nn.Module, **kwargs - ) -> None: - super().__init__() - self.layers = nn.ModuleList() - for _ in range(num_heads): - self.layers.append(layer(**kwargs)) - self.num_heads = num_heads - self.readout = readout - -
[docs] def forward(self, **kwargs) -> torch.Tensor: - r"""The forward function. - - .. note:: - You must explicitly pass the keyword arguments to the layer. For example, if the layer is ``GATConv``, you must pass ``X=X`` and ``g=g``. - """ - if self.readout == "concat": - return torch.cat([layer(**kwargs) for layer in self.layers], dim=-1) - else: - outs = torch.stack([layer(**kwargs) for layer in self.layers]) - if self.readout == "mean": - return outs.mean(dim=0) - elif self.readout == "max": - return outs.max(dim=0)[0] - elif self.readout == "sum": - return outs.sum(dim=0) - else: - raise ValueError("Unknown readout type")
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/hypergraphs/dhcf_conv.html b/docs/_modules/easygraph/nn/convs/hypergraphs/dhcf_conv.html deleted file mode 100644 index 2b09e4fb..00000000 --- a/docs/_modules/easygraph/nn/convs/hypergraphs/dhcf_conv.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - easygraph.nn.convs.hypergraphs.dhcf_conv — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.nn.convs.hypergraphs.dhcf_conv

-import torch
-import torch.nn as nn
-
-from easygraph.classes import Hypergraph
-
-
-
[docs]class JHConv(nn.Module): - r"""The Jump Hypergraph Convolution layer proposed in `Dual Channel Hypergraph Collaborative Filtering <https://dl.acm.org/doi/10.1145/3394486.3403253>`_ paper (KDD 2020). - - Matrix Format: - - .. math:: - \mathbf{X}^{\prime} = \sigma \left( \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} - \mathbf{H}^\top \mathbf{D}_v^{-\frac{1}{2}} \mathbf{X} \mathbf{\Theta} + \mathbf{X} \right). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): If set to a positive number, the layer will use dropout. Defaults to ``0.5``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.act = nn.ReLU(inplace=True) - self.drop = nn.Dropout(drop_rate) - self.theta = nn.Linear(in_channels, out_channels, bias=bias) - -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - - Args: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - hg (``dhg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - X = self.theta(X) - if self.bn is not None: - X = self.bn(X) - X = hg.smoothing_with_HGNN(X) + X - if not self.is_last: - X = self.drop(self.act(X)) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/hypergraphs/halfnlh_conv.html b/docs/_modules/easygraph/nn/convs/hypergraphs/halfnlh_conv.html deleted file mode 100644 index a6b0f400..00000000 --- a/docs/_modules/easygraph/nn/convs/hypergraphs/halfnlh_conv.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - easygraph.nn.convs.hypergraphs.halfnlh_conv — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.nn.convs.hypergraphs.halfnlh_conv

-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-
-from easygraph.nn.convs.common import MLP
-from easygraph.nn.convs.pma import PMA
-from torch_geometric.nn.conv import MessagePassing
-from torch_scatter import scatter
-
-
-
[docs]class HalfNLHconv(MessagePassing): - r"""The HalfNLHconv model proposed in `YOU ARE ALLSET: A MULTISET LEARNING FRAMEWORK FOR HYPERGRAPH NEURAL NETWORKS <https://openreview.net/pdf?id=hpBTIv2uy_E>`_ paper (ICLR 2022). - - Parameters: - ``in_dim`` (``int``): : The dimension of input. - ``hid_dim`` (``int``): : The dimension of hidden. - ``out_dim`` (``int``): : The dimension of output. - ``num_layers`` (``int``): : The number of layers. - ``dropout`` (``float``): Dropout ratio. Defaults to 0.5. - ``normalization`` (``str``): The normalization method. Defaults to ``bn`` - ``InputNorm`` (``bool``): Defaults to False. - ``heads`` (``int``): Defaults to 1 - `attention`` (``bool``): Defaults to True - - """ - - def __init__( - self, - in_dim, - hid_dim, - out_dim, - num_layers, - dropout, - normalization="bn", - InputNorm=False, - heads=1, - attention=True, - ): - super(HalfNLHconv, self).__init__() - - self.attention = attention - self.dropout = dropout - - if self.attention: - self.prop = PMA(in_dim, hid_dim, out_dim, num_layers, heads=heads) - else: - if num_layers > 0: - self.f_enc = MLP( - in_dim, - hid_dim, - hid_dim, - num_layers, - dropout, - normalization, - InputNorm, - ) - self.f_dec = MLP( - hid_dim, - hid_dim, - out_dim, - num_layers, - dropout, - normalization, - InputNorm, - ) - else: - self.f_enc = nn.Identity() - self.f_dec = nn.Identity() - -
[docs] def reset_parameters(self): - if self.attention: - self.prop.reset_parameters() - else: - if not (self.f_enc.__class__.__name__ is "Identity"): - self.f_enc.reset_parameters() - if not (self.f_dec.__class__.__name__ is "Identity"): - self.f_dec.reset_parameters()
- - # self.bn.reset_parameters() - -
[docs] def forward(self, x, edge_index, norm, aggr="add"): - """ - input -> MLP -> Prop - """ - - if self.attention: - x = self.prop(x, edge_index) - else: - x = F.relu(self.f_enc(x)) - x = F.dropout(x, p=self.dropout, training=self.training) - x = self.propagate(edge_index, x=x, norm=norm, aggr=aggr) - x = F.relu(self.f_dec(x)) - - return x
- -
[docs] def message(self, x_j, norm): - return norm.view(-1, 1) * x_j
- -
[docs] def aggregate(self, inputs, index, dim_size=None, aggr="sum"): - r"""Aggregates messages from neighbors as - :math:`\square_{j \in \mathcal{N}(i)}`. - - Takes in the output of message computation as first argument and any - argument which was initially passed to :meth:`propagate`. - - By default, this function will delegate its call to scatter functions - that support "add", "mean" and "max" operations as specified in - :meth:`__init__` by the :obj:`aggr` argument. - """ - # ipdb.set_trace() - - return scatter(inputs, index, dim=self.node_dim, reduce=aggr)
-
- -
-
- -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/hypergraphs/hgnn_conv.html b/docs/_modules/easygraph/nn/convs/hypergraphs/hgnn_conv.html deleted file mode 100644 index 72bd5600..00000000 --- a/docs/_modules/easygraph/nn/convs/hypergraphs/hgnn_conv.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - easygraph.nn.convs.hypergraphs.hgnn_conv — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.nn.convs.hypergraphs.hgnn_conv

-import torch
-import torch.nn as nn
-
-from easygraph.classes import Hypergraph
-
-
-
[docs]class HGNNConv(nn.Module): - r"""The HGNN convolution layer proposed in `Hypergraph Neural Networks <https://arxiv.org/pdf/1809.09401>`_ paper (AAAI 2019). - Matrix Format: - - .. math:: - \mathbf{X}^{\prime} = \sigma \left( \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \mathbf{W}_e \mathbf{D}_e^{-1} - \mathbf{H}^\top \mathbf{D}_v^{-\frac{1}{2}} \mathbf{X} \mathbf{\Theta} \right). - - where :math:`\mathbf{X}` is the input vertex feature matrix, :math:`\mathbf{H}` is the hypergraph incidence matrix, - :math:`\mathbf{W}_e` is a diagonal hyperedge weight matrix, :math:`\mathbf{D}_v` is a diagonal vertex degree matrix, - :math:`\mathbf{D}_e` is a diagonal hyperedge degree matrix, :math:`\mathbf{\Theta}` is the learnable parameters. - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): If set to a positive number, the layer will use dropout. Defaults to ``0.5``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.act = nn.ReLU(inplace=True) - self.drop = nn.Dropout(drop_rate) - self.theta = nn.Linear(in_channels, out_channels, bias=bias) - -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - - Args: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - hg (``dhg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - X = self.theta(X) - if self.bn is not None: - X = self.bn(X) - X = hg.smoothing_with_HGNN(X) - if not self.is_last: - X = self.drop(self.act(X)) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/hypergraphs/hgnnp_conv.html b/docs/_modules/easygraph/nn/convs/hypergraphs/hgnnp_conv.html deleted file mode 100644 index 4e7ef4a5..00000000 --- a/docs/_modules/easygraph/nn/convs/hypergraphs/hgnnp_conv.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - easygraph.nn.convs.hypergraphs.hgnnp_conv — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.nn.convs.hypergraphs.hgnnp_conv

-import torch
-import torch.nn as nn
-
-from easygraph.classes import Hypergraph
-
-
-
[docs]class HGNNPConv(nn.Module): - r"""The HGNN :sup:`+` convolution layer proposed in `HGNN+: General Hypergraph Neural Networks <https://ieeexplore.ieee.org/document/9795251>`_ paper (IEEE T-PAMI 2022). - - Sparse Format: - - .. math:: - - \left\{ - \begin{aligned} - m_{\beta}^{t} &=\sum_{\alpha \in \mathcal{N}_{v}(\beta)} M_{v}^{t}\left(x_{\alpha}^{t}\right) \\ - y_{\beta}^{t} &=U_{e}^{t}\left(w_{\beta}, m_{\beta}^{t}\right) \\ - m_{\alpha}^{t+1} &=\sum_{\beta \in \mathcal{N}_{e}(\alpha)} M_{e}^{t}\left(x_{\alpha}^{t}, y_{\beta}^{t}\right) \\ - x_{\alpha}^{t+1} &=U_{v}^{t}\left(x_{\alpha}^{t}, m_{\alpha}^{t+1}\right) \\ - \end{aligned} - \right. - - Matrix Format: - - .. math:: - \mathbf{X}^{\prime} = \sigma \left( \mathbf{D}_v^{-1} \mathbf{H} \mathbf{W}_e - \mathbf{D}_e^{-1} \mathbf{H}^\top \mathbf{X} \mathbf{\Theta} \right). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): If set to a positive number, the layer will use dropout. Defaults to ``0.5``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.act = nn.ReLU(inplace=True) - self.drop = nn.Dropout(drop_rate) - self.theta = nn.Linear(in_channels, out_channels, bias=bias) - -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - - Args: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(|\mathcal{V}|, C_{in})`. - hg (``dhg.Hypergraph``): The hypergraph structure that contains :math:`|\mathcal{V}|` vertices. - """ - X = self.theta(X) - if self.bn is not None: - X = self.bn(X) - X = hg.v2v(X, aggr="mean") - if not self.is_last: - X = self.drop(self.act(X)) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/hypergraphs/hnhn_conv.html b/docs/_modules/easygraph/nn/convs/hypergraphs/hnhn_conv.html deleted file mode 100644 index 5d24dc32..00000000 --- a/docs/_modules/easygraph/nn/convs/hypergraphs/hnhn_conv.html +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - easygraph.nn.convs.hypergraphs.hnhn_conv — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.nn.convs.hypergraphs.hnhn_conv

-import torch
-import torch.nn as nn
-
-from easygraph.classes import Hypergraph
-
-
-
[docs]class HNHNConv(nn.Module): - r"""The HNHN convolution layer proposed in `HNHN: Hypergraph Networks with Hyperedge Neurons <https://arxiv.org/pdf/2006.12278.pdf>`_ paper (ICML 2020). - - Args: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): If set to a positive number, the layer will use dropout. Defaults to ``0.5``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.act = nn.ReLU(inplace=True) - self.drop = nn.Dropout(drop_rate) - self.theta_v2e = nn.Linear(in_channels, out_channels, bias=bias) - self.theta_e2v = nn.Linear(out_channels, out_channels, bias=bias) - -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - - Args: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(|\mathcal{V}|, C_{in})`. - hg (``dhg.Hypergraph``): The hypergraph structure that contains :math:`|\mathcal{V}|` vertices. - """ - # v -> e - X = self.theta_v2e(X) - if self.bn is not None: - X = self.bn(X) - Y = self.act(hg.v2e(X, aggr="mean")) - # e -> v - Y = self.theta_e2v(Y) - X = hg.e2v(Y, aggr="mean") - if not self.is_last: - X = self.drop(self.act(X)) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/hypergraphs/hwnn_conv.html b/docs/_modules/easygraph/nn/convs/hypergraphs/hwnn_conv.html deleted file mode 100644 index a44dba13..00000000 --- a/docs/_modules/easygraph/nn/convs/hypergraphs/hwnn_conv.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - easygraph.nn.convs.hypergraphs.hwnn_conv — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.nn.convs.hypergraphs.hwnn_conv

-import torch
-import torch.nn as nn
-
-from easygraph.classes import Hypergraph
-
-
-
[docs]class HWNNConv(nn.Module): - r"""The HWNNConv model proposed in `Heterogeneous Hypergraph Embedding for Graph Classification <https://arxiv.org/pdf/2010.10728>`_ paper (ACM 2021). - - Parameters: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (``int``): :math:`C_{out}` is the number of output channels. - ``ncount`` (``int``): The Number of node in the hypergraph. - ``K1`` (``int``): Polynomial calculation times. - ``K2`` (``int``): Polynomial calculation times. - ``approx`` (``bool``): Whether to use polynomial fitting - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - ncount: int, - K1: int = 2, - K2: int = 2, - approx: bool = False, - ): - super().__init__() - self.in_channels = in_channels - self.out_channels = out_channels - self.K1 = K1 - self.K2 = K2 - self.ncount = ncount - self.approx = approx - self.W = torch.nn.Parameter(torch.Tensor(self.in_channels, self.out_channels)) - self.W_d = torch.nn.Parameter(torch.Tensor(self.ncount)) - self.par = torch.nn.Parameter(torch.Tensor(self.K1 + self.K2)) - self.init_parameters() - -
[docs] def init_parameters(self): - torch.nn.init.xavier_uniform_(self.W) - torch.nn.init.uniform_(self.W_d, 0.99, 1.01) - torch.nn.init.uniform_(self.par, 0, 0.99)
- -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - Parameters: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - hg (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - """ - if self.approx == True: - X = hg.smoothing_with_HWNN_approx( - X, self.par, self.W_d, self.K1, self.K2, self.W - ) - else: - X = hg.smoothing_with_HWNN_wavelet(X, self.W_d, self.W) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/hypergraphs/hypergcn_conv.html b/docs/_modules/easygraph/nn/convs/hypergraphs/hypergcn_conv.html deleted file mode 100644 index 96e8b538..00000000 --- a/docs/_modules/easygraph/nn/convs/hypergraphs/hypergcn_conv.html +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - easygraph.nn.convs.hypergraphs.hypergcn_conv — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.nn.convs.hypergraphs.hypergcn_conv

-from typing import Optional
-
-import torch
-import torch.nn as nn
-
-from easygraph.classes import Graph
-from easygraph.classes import Hypergraph
-
-
-
[docs]class HyperGCNConv(nn.Module): - r"""The HyperGCN convolution layer proposed in `HyperGCN: A New Method of Training Graph Convolutional Networks on Hypergraphs <https://papers.nips.cc/paper/2019/file/1efa39bcaec6f3900149160693694536-Paper.pdf>`_ paper (NeurIPS 2019). - - Parameters: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``use_mediator`` (``str``): Whether to use mediator to transform the hyperedges to edges in the graph. Defaults to ``False``. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): If set to a positive number, the layer will use dropout. Defaults to ``0.5``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - use_mediator: bool = False, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.use_mediator = use_mediator - self.act = nn.ReLU(inplace=True) - self.drop = nn.Dropout(drop_rate) - self.theta = nn.Linear(in_channels, out_channels, bias=bias) - -
[docs] def forward( - self, X: torch.Tensor, hg: Hypergraph, cached_g: Optional[Graph] = None - ) -> torch.Tensor: - r"""The forward function. - - Parameters: - ``X`` (``torch.Tensor``): Input vertex feature matrix. Size :math:`(N, C_{in})`. - ``hg`` (``eg.Hypergraph``): The hypergraph structure that contains :math:`N` vertices. - ``cached_g`` (``eg.Graph``): The pre-transformed graph structure from the hypergraph structure that contains :math:`N` vertices. If not provided, the graph structure will be transformed for each forward time. Defaults to ``None``. - """ - X = self.theta(X) - if self.bn is not None: - X = self.bn(X) - if cached_g is None: - g = Graph.from_hypergraph_hypergcn(hg, X, self.use_mediator) - X = g.smoothing_with_GCN(X) - else: - X = cached_g.smoothing_with_GCN(X) - if not self.is_last: - X = self.drop(self.act(X)) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/hypergraphs/unignn_conv.html b/docs/_modules/easygraph/nn/convs/hypergraphs/unignn_conv.html deleted file mode 100644 index 117108f1..00000000 --- a/docs/_modules/easygraph/nn/convs/hypergraphs/unignn_conv.html +++ /dev/null @@ -1,402 +0,0 @@ - - - - - - easygraph.nn.convs.hypergraphs.unignn_conv — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.nn.convs.hypergraphs.unignn_conv

-import torch
-import torch.nn as nn
-
-from easygraph.classes import Hypergraph
-
-
-
[docs]class UniGCNConv(nn.Module): - r"""The UniGCN convolution layer proposed in `UniGNN: a Unified Framework for Graph and Hypergraph Neural Networks <https://arxiv.org/pdf/2105.00956.pdf>`_ paper (IJCAI 2021). - - Sparse Format: - - .. math:: - \left\{ - \begin{aligned} - h_{e} &= \frac{1}{|e|} \sum_{j \in e} x_{j} \\ - \tilde{x}_{i} &= \frac{1}{\sqrt{d_{i}}} \sum_{e \in \tilde{E}_{i}} \frac{1}{\sqrt{\tilde{d}_{e}}} W h_{e} - \end{aligned} - \right. . - - where :math:`\tilde{d}_{e} = \frac{1}{|e|} \sum_{i \in e} d_{i}`. - - Matrix Format: - - .. math:: - \mathbf{X}^{\prime} = \sigma \left( \mathbf{D}_v^{-\frac{1}{2}} \mathbf{H} \tilde{\mathbf{D}}_e^{-\frac{1}{2}} \cdot \mathbf{D}_e^{-1} \mathbf{H}^\top \mathbf{X} \mathbf{\Theta} \right) . - - Parameters: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): If set to a positive number, the layer will use dropout. Defaults to ``0.5``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.act = nn.ReLU(inplace=True) - self.drop = nn.Dropout(drop_rate) - self.theta = nn.Linear(in_channels, out_channels, bias=bias) - -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - - Parameters: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(|\mathcal{V}|, C_{in})`. - hg (``eg.Hypergraph``): The hypergraph structure that contains :math:`|\mathcal{V}|` vertices. - """ - X = self.theta(X) - Y = hg.v2e(X, aggr="mean") - # compute the special degree of hyperedges - # _De = torch.zeros(hg.num_e, device=hg.device) - _De = torch.zeros(hg.num_e) - _Dv = hg.D_v._values()[hg.v2e_src] - _De = ( - _De.scatter_reduce(0, index=hg.v2e_dst, src=_Dv, reduce="mean") - / _De.scatter_reduce( - 0, index=hg.v2e_dst, src=(_Dv != 0).float(), reduce="sum" - ) - ).pow(-0.5) - - _De[_De.isinf()] = 1 - Y = _De.view(-1, 1) * Y - # =============================================== - X = hg.e2v(Y, aggr="sum") - X = torch.sparse.mm(hg.D_v_neg_1_2, X) - - if not self.is_last: - X = self.act(X) - if self.bn is not None: - X = self.bn(X) - X = self.drop(X) - return X
- - -
[docs]class UniGATConv(nn.Module): - r"""The UniGAT convolution layer proposed in `UniGNN: a Unified Framework for Graph and Hypergraph Neural Networks <https://arxiv.org/pdf/2105.00956.pdf>`_ paper (IJCAI 2021). - - Sparse Format: - - .. math:: - \left\{ - \begin{aligned} - \alpha_{i e} &=\sigma\left(a^{T}\left[W h_{\{i\}} ; W h_{e}\right]\right) \\ - \tilde{\alpha}_{i e} &=\frac{\exp \left(\alpha_{i e}\right)}{\sum_{e^{\prime} \in \tilde{E}_{i}} \exp \left(\alpha_{i e^{\prime}}\right)} \\ - \tilde{x}_{i} &=\sum_{e \in \tilde{E}_{i}} \tilde{\alpha}_{i e} W h_{e} - \end{aligned} - \right. . - - Parameters: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): The dropout probability. If ``dropout <= 0``, the layer will not drop values. Defaults to ``0.5``. - ``atten_neg_slope`` (``float``): Hyper-parameter of the ``LeakyReLU`` activation of edge attention. Defaults to ``0.2``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - atten_neg_slope: float = 0.2, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.atten_dropout = nn.Dropout(drop_rate) - self.atten_act = nn.LeakyReLU(atten_neg_slope) - self.act = nn.ELU(inplace=True) - self.theta = nn.Linear(in_channels, out_channels, bias=bias) - self.atten_e = nn.Linear(out_channels, 1, bias=False) - self.atten_dst = nn.Linear(out_channels, 1, bias=False) - -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - - Parameters: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(|\mathcal{V}|, C_{in})`. - hg (``eg.Hypergraph``): The hypergraph structure that contains :math:`|\mathcal{V}|` vertices. - """ - X = self.theta(X) - Y = hg.v2e(X, aggr="mean") - # =============================================== - # alpha_e = self.atten_e(Y) - # e_atten_score = alpha_e[hg.e2v_src] - # e_atten_score = self.atten_dropout(self.atten_act(e_atten_score).squeeze()) - - e_atten_score = self.atten_dropout( - self.atten_act(self.atten_e(Y)[hg.e2v_src]).squeeze() - ) - - # ================================================================================ - # We suggest to add a clamp on attention weight to avoid Nan error in training. - e_atten_score.clamp_(min=0.001, max=5) - # ================================================================================ - X = hg.e2v(Y, aggr="softmax_then_sum", e2v_weight=e_atten_score) - - if not self.is_last: - X = self.act(X) - if self.bn is not None: - X = self.bn(X) - return X
- - -
[docs]class UniSAGEConv(nn.Module): - r"""The UniSAGE convolution layer proposed in `UniGNN: a Unified Framework for Graph and Hypergraph Neural Networks <https://arxiv.org/pdf/2105.00956.pdf>`_ paper (IJCAI 2021). - - Sparse Format: - - .. math:: - \left\{ - \begin{aligned} - h_{e} &= \frac{1}{|e|} \sum_{j \in e} x_{j} \\ - \tilde{x}_{i} &= W\left(x_{i}+\text { AGGREGATE }\left(\left\{x_{j}\right\}_{j \in \mathcal{N}_{i}}\right)\right) - \end{aligned} - \right. . - - Matrix Format: - - .. math:: - \mathbf{X}^{\prime} = \sigma \left( \left( \mathbf{I} + \mathbf{H} \mathbf{D}_e^{-1} \mathbf{H}^\top \right) \mathbf{X} \mathbf{\Theta} \right) . - - Parameters: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): If set to a positive number, the layer will use dropout. Defaults to ``0.5``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.act = nn.ReLU(inplace=True) - self.drop = nn.Dropout(drop_rate) - self.theta = nn.Linear(in_channels, out_channels, bias=bias) - -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - - Parameters: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(|\mathcal{V}|, C_{in})`. - hg (``eg.Hypergraph``): The hypergraph structure that contains :math:`|\mathcal{V}|` vertices. - """ - X = self.theta(X) - Y = hg.v2e(X, aggr="mean") - X = hg.e2v(Y, aggr="sum") + X - if not self.is_last: - X = self.act(X) - if self.bn is not None: - X = self.bn(X) - X = self.drop(X) - return X
- - -
[docs]class UniGINConv(nn.Module): - r"""The UniGIN convolution layer proposed in `UniGNN: a Unified Framework for Graph and Hypergraph Neural Networks <https://arxiv.org/pdf/2105.00956.pdf>`_ paper (IJCAI 2021). - - Sparse Format: - - .. math:: - - \left\{ - \begin{aligned} - h_{e} &= \frac{1}{|e|} \sum_{j \in e} x_{j} \\ - \tilde{x}_{i} &= W\left((1+\varepsilon) x_{i}+\sum_{e \in E_{i}} h_{e}\right) - \end{aligned} - \right. . - - Matrix Format: - - .. math:: - \mathbf{X}^{\prime} = \sigma \left( \left( \left( \mathbf{I} + \varepsilon \right) + \mathbf{H} \mathbf{D}_e^{-1} \mathbf{H}^\top \right) \mathbf{X} \mathbf{\Theta} \right) . - - Parameters: - ``in_channels`` (``int``): :math:`C_{in}` is the number of input channels. - ``out_channels`` (int): :math:`C_{out}` is the number of output channels. - ``eps`` (``float``): :math:`\varepsilon` is the learnable parameter. Defaults to ``0.0``. - ``train_eps`` (``bool``): If set to ``True``, the layer will learn the :math:`\varepsilon` parameter. Defaults to ``False``. - ``bias`` (``bool``): If set to ``False``, the layer will not learn the bias parameter. Defaults to ``True``. - ``use_bn`` (``bool``): If set to ``True``, the layer will use batch normalization. Defaults to ``False``. - ``drop_rate`` (``float``): If set to a positive number, the layer will use dropout. Defaults to ``0.5``. - ``is_last`` (``bool``): If set to ``True``, the layer will not apply the final activation and dropout functions. Defaults to ``False``. - """ - - def __init__( - self, - in_channels: int, - out_channels: int, - eps: float = 0.0, - train_eps: bool = False, - bias: bool = True, - use_bn: bool = False, - drop_rate: float = 0.5, - is_last: bool = False, - ): - super().__init__() - self.is_last = is_last - if train_eps: - self.eps = nn.Parameter(torch.tensor([eps])) - else: - self.eps = eps - self.bn = nn.BatchNorm1d(out_channels) if use_bn else None - self.act = nn.ReLU(inplace=True) - self.drop = nn.Dropout(drop_rate) - self.theta = nn.Linear(in_channels, out_channels, bias=bias) - -
[docs] def forward(self, X: torch.Tensor, hg: Hypergraph) -> torch.Tensor: - r"""The forward function. - - Parameters: - X (``torch.Tensor``): Input vertex feature matrix. Size :math:`(|\mathcal{V}|, C_{in})`. - hg (``eg.Hypergraph``): The hypergraph structure that contains :math:`|\mathcal{V}|` vertices. - """ - X = self.theta(X) - Y = hg.v2e(X, aggr="mean") - X = (1 + self.eps) * hg.e2v(Y, aggr="sum") + X - if not self.is_last: - X = self.act(X) - if self.bn is not None: - X = self.bn(X) - X = self.drop(X) - return X
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/convs/pma.html b/docs/_modules/easygraph/nn/convs/pma.html deleted file mode 100644 index 1edc631c..00000000 --- a/docs/_modules/easygraph/nn/convs/pma.html +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - - - easygraph.nn.convs.pma — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.nn.convs.pma

-import math
-
-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-
-from easygraph.nn.convs.common import MLP
-from torch import Tensor
-from torch.nn import Linear
-from torch.nn import Parameter
-from torch_geometric.nn.conv import MessagePassing
-from torch_geometric.typing import Adj
-from torch_geometric.typing import OptTensor
-from torch_geometric.typing import Size
-from torch_geometric.typing import SparseTensor
-from torch_geometric.utils import softmax
-from torch_scatter import scatter
-
-
-
[docs]def glorot(tensor): - if tensor is not None: - stdv = math.sqrt(6.0 / (tensor.size(-2) + tensor.size(-1))) - tensor.data.uniform_(-stdv, stdv)
- - -
[docs]def zeros(tensor): - if tensor is not None: - tensor.data.fill_(0)
- - -
[docs]class PMA(MessagePassing): - """ - PMA part: - Note that in original PMA, we need to compute the inner product of the seed and neighbor nodes. - i.e. e_ij = a(Wh_i,Wh_j), where a should be the inner product, h_i is the seed and h_j are neightbor nodes. - In GAT, a(x,y) = a^T[x||y]. We use the same logic. - """ - - _alpha: OptTensor - - def __init__( - self, - in_channels, - hid_dim, - out_channels, - num_layers, - heads=1, - concat=True, - negative_slope=0.2, - dropout=0.0, - bias=False, - **kwargs, - ): - super(PMA, self).__init__(node_dim=0, **kwargs) - - self.in_channels = in_channels - self.hidden = hid_dim // heads - self.out_channels = out_channels - self.heads = heads - self.concat = concat - self.negative_slope = negative_slope - self.dropout = dropout - self.aggr = "add" - # self.input_seed = input_seed - - # This is the encoder part. Where we use 1 layer NN (Theta*x_i in the GATConv description) - # Now, no seed as input. Directly learn the importance weights alpha_ij. - # self.lin_O = Linear(heads*self.hidden, self.hidden) # For heads combining - # For neighbor nodes (source side, key) - self.lin_K = Linear(in_channels, self.heads * self.hidden) - # For neighbor nodes (source side, value) - self.lin_V = Linear(in_channels, self.heads * self.hidden) - self.att_r = Parameter(torch.Tensor(1, heads, self.hidden)) # Seed vector - self.rFF = MLP( - in_channels=self.heads * self.hidden, - hidden_channels=self.heads * self.hidden, - out_channels=out_channels, - num_layers=num_layers, - dropout=0.0, - normalization="None", - ) - self.ln0 = nn.LayerNorm(self.heads * self.hidden) - self.ln1 = nn.LayerNorm(self.heads * self.hidden) - # if bias and concat: - # self.bias = Parameter(torch.Tensor(heads * out_channels)) - # elif bias and not concat: - # self.bias = Parameter(torch.Tensor(out_channels)) - # else: - - # Always no bias! (For now) - self.register_parameter("bias", None) - - self._alpha = None - - self.reset_parameters() - -
[docs] def reset_parameters(self): - glorot(self.lin_K.weight) - glorot(self.lin_V.weight) - self.rFF.reset_parameters() - self.ln0.reset_parameters() - self.ln1.reset_parameters() - # glorot(self.att_l) - nn.init.xavier_uniform_(self.att_r)
- - # zeros(self.bias) - -
[docs] def forward( - self, x, edge_index: Adj, size: Size = None, return_attention_weights=None - ): - r""" - Args: - return_attention_weights (bool, optional): If set to :obj:`True`, - will additionally return the tuple - :obj:`(edge_index, attention_weights)`, holding the computed - attention weights for each edge. (default: :obj:`None`) - """ - H, C = self.heads, self.hidden - - x_l: OptTensor = None - x_r: OptTensor = None - alpha_l: OptTensor = None - alpha_r: OptTensor = None - if isinstance(x, Tensor): - assert x.dim() == 2, "Static graphs not supported in `GATConv`." - x_K = self.lin_K(x).view(-1, H, C) - x_V = self.lin_V(x).view(-1, H, C) - alpha_r = (x_K * self.att_r).sum(dim=-1) - - out = self.propagate(edge_index, x=x_V, alpha=alpha_r, aggr=self.aggr) - - alpha = self._alpha - self._alpha = None - - # Note that in the original code of GMT paper, they do not use additional W^O to combine heads. - # This is because O = softmax(QK^T)V and V = V_in*W^V. So W^O can be effectively taken care by W^V!!! - out += self.att_r # This is Seed + Multihead - # concat heads then LayerNorm. Z (rhs of Eq(7)) in GMT paper. - out = self.ln0(out.view(-1, self.heads * self.hidden)) - # rFF and skip connection. Lhs of eq(7) in GMT paper. - out = self.ln1(out + F.relu(self.rFF(out))) - - if isinstance(return_attention_weights, bool): - assert alpha is not None - if isinstance(edge_index, Tensor): - return out, (edge_index, alpha) - elif isinstance(edge_index, SparseTensor): - return out, edge_index.set_value(alpha, layout="coo") - else: - return out
- -
[docs] def message(self, x_j, alpha_j, index, ptr, size_j): - # ipdb.set_trace() - alpha = alpha_j - alpha = F.leaky_relu(alpha, self.negative_slope) - alpha = softmax(alpha, index, ptr, index.max() + 1) - self._alpha = alpha - alpha = F.dropout(alpha, p=self.dropout, training=self.training) - return x_j * alpha.unsqueeze(-1)
- -
[docs] def aggregate(self, inputs, index, dim_size=None, aggr="add"): - r"""Aggregates messages from neighbors as - :math:`\square_{j \in \mathcal{N}(i)}`. - - Takes in the output of message computation as first argument and any - argument which was initially passed to :meth:`propagate`. - - By default, this function will delegate its call to scatter functions - that support "add", "mean" and "max" operations as specified in - :meth:`__init__` by the :obj:`aggr` argument. - """ - # ipdb.set_trace() - if aggr is None: - raise ValueError("aggr was not passed!") - return scatter(inputs, index, dim=self.node_dim, reduce=aggr)
- - def __repr__(self): - return "{}({}, {}, heads={})".format( - self.__class__.__name__, self.in_channels, self.out_channels, self.heads - )
-
- -
-
- -
-
-
-
- - - - - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/loss.html b/docs/_modules/easygraph/nn/loss.html deleted file mode 100644 index 6d9ff82a..00000000 --- a/docs/_modules/easygraph/nn/loss.html +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - easygraph.nn.loss — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.nn.loss

-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-
-
-
[docs]class BPRLoss(nn.Module): - r"""This criterion computes the Bayesian Personalized Ranking (BPR) loss between the positive scores and the negative scores. - - Args: - ``alpha`` (``float``, optional): The weight for the positive scores in the BPR loss. Defaults to ``1.0``. - ``beta`` (``float``, optional): The weight for the negative scores in the BPR loss. Defaults to ``1.0``. - ``activation`` (``str``, optional): The activation function to use can be one of ``"sigmoid_then_log"``, ``"softplus"``. Defaults to ``"sigmoid_then_log"``. - """ - - def __init__( - self, - alpha: float = 1.0, - beta: float = 1.0, - activation: str = "sigmoid_then_log", - ): - super().__init__() - assert activation in ( - "sigmoid_then_log", - "softplus", - ), "activation function of BPRLoss must be sigmoid_then_log or softplus." - self.activation = activation - self.alpha = alpha - self.beta = beta - -
[docs] def forward(self, pos_scores: torch.Tensor, neg_scores: torch.Tensor): - r"""The forward function of BPRLoss. - - Args: - ``pos_scores`` (``torch.Tensor``): The positive scores. - ``neg_scores`` (``torch.Tensor``): The negative scores. - """ - if self.activation == "sigmoid_then_log": - loss = -(self.alpha * pos_scores - self.beta * neg_scores).sigmoid().log() - elif self.activation == "softplus": - loss = F.softplus(self.beta * neg_scores - self.alpha * pos_scores) - else: - raise NotImplementedError - return loss.mean()
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/nn/regularization.html b/docs/_modules/easygraph/nn/regularization.html deleted file mode 100644 index fac1a119..00000000 --- a/docs/_modules/easygraph/nn/regularization.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - easygraph.nn.regularization — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.nn.regularization

-from typing import List
-
-import torch
-import torch.nn as nn
-
-
-
[docs]class EmbeddingRegularization(nn.Module): - r"""Regularization function for embeddings. - - Args: - ``p`` (``int``): The power to use in the regularization. Defaults to ``2``. - ``weight_decay`` (``float``): The weight of the regularization. Defaults to ``1e-4``. - """ - - def __init__(self, p: int = 2, weight_decay: float = 1e-4): - super().__init__() - self.p = p - self.weight_decay = weight_decay - -
[docs] def forward(self, *embs: List[torch.Tensor]): - r"""The forward function. - - Args: - ``embs`` (``List[torch.Tensor]``): The input embeddings. - """ - loss = 0 - for emb in embs: - loss += 1 / self.p * emb.pow(self.p).sum(dim=1).mean() - return self.weight_decay * loss
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/edgelist.html b/docs/_modules/easygraph/readwrite/edgelist.html deleted file mode 100644 index acbd7fef..00000000 --- a/docs/_modules/easygraph/readwrite/edgelist.html +++ /dev/null @@ -1,576 +0,0 @@ - - - - - - easygraph.readwrite.edgelist — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.readwrite.edgelist

-import easygraph as eg
-
-from easygraph.utils import open_file
-
-
-__all__ = [
-    "parse_edgelist",
-    "generate_edgelist",
-    "write_edgelist",
-    "read_edgelist",
-    "read_weighted_edgelist",
-    "write_weighted_edgelist",
-]
-
-
-
[docs]def parse_edgelist( - lines, comments="#", delimiter=None, create_using=None, nodetype=None, data=True -): - """Parse lines of an edge list representation of a graph. - - Parameters - ---------- - lines : list or iterator of strings - Input data in edgelist format - comments : string, optional - Marker for comment lines. Default is `'#'`. To specify that no character - should be treated as a comment, use ``comments=None``. - delimiter : string, optional - Separator for node labels. Default is `None`, meaning any whitespace. - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - nodetype : Python type, optional - Convert nodes to this type. Default is `None`, meaning no conversion is - performed. - data : bool or list of (label,type) tuples - If `False` generate no edge data or if `True` use a dictionary - representation of edge data or a list tuples specifying dictionary - key names and types for edge data. - - Returns - ------- - G: EasyGraph Graph - The graph corresponding to lines - - Examples - -------- - Edgelist with no data: - - >>> lines = ["1 2", "2 3", "3 4"] - >>> G = eg.parse_edgelist(lines, nodetype=int) - >>> list(G) - [1, 2, 3, 4] - >>> list(G.edges) - [(1, 2), (2, 3), (3, 4)] - - Edgelist with data in Python dictionary representation: - - >>> lines = ["1 2 {'weight': 3}", "2 3 {'weight': 27}", "3 4 {'weight': 3.0}"] - >>> G = eg.parse_edgelist(lines, nodetype=int) - >>> list(G) - [1, 2, 3, 4] - >>> list(G.edges) - [(1, 2, {'weight': 3}), (2, 3, {'weight': 27}), (3, 4, {'weight': 3.0})] - - Edgelist with data in a list: - - >>> lines = ["1 2 3", "2 3 27", "3 4 3.0"] - >>> G = eg.parse_edgelist(lines, nodetype=int, data=(("weight", float),)) - >>> list(G) - [1, 2, 3, 4] - >>> list(G.edges) - [(1, 2, {'weight': 3.0}), (2, 3, {'weight': 27.0}), (3, 4, {'weight': 3.0})] - - See Also - -------- - read_weighted_edgelist - """ - from ast import literal_eval - - G = eg.empty_graph(0, create_using) - for line in lines: - if comments is not None: - p = line.find(comments) - if p >= 0: - line = line[:p] - if not line: - continue - # split line, should have 2 or more - s = line.strip().split(delimiter) - if len(s) < 2: - continue - u = s.pop(0) - v = s.pop(0) - d = s - if nodetype is not None: - try: - u = nodetype(u) - v = nodetype(v) - except Exception as err: - raise TypeError( - f"Failed to convert nodes {u},{v} to type {nodetype}." - ) from err - - if len(d) == 0 or data is False: - # no data or data type specified - edgedata = {} - elif data is True: - # no edge types specified - try: # try to evaluate as dictionary - if delimiter == ",": - edgedata_str = ",".join(d) - else: - edgedata_str = " ".join(d) - edgedata = dict(literal_eval(edgedata_str.strip())) - except Exception as err: - raise TypeError( - f"Failed to convert edge data ({d}) to dictionary." - ) from err - else: - # convert edge data to dictionary with specified keys and type - if len(d) != len(data): - raise IndexError( - f"Edge data {d} and data_keys {data} are not the same length" - ) - edgedata = {} - for (edge_key, edge_type), edge_value in zip(data, d): - try: - edge_value = edge_type(edge_value) - except Exception as err: - raise TypeError( - f"Failed to convert {edge_key} data {edge_value} " - f"to type {edge_type}." - ) from err - edgedata.update({edge_key: edge_value}) - G.add_edge(u, v, **edgedata) - return G
- - -
[docs]def generate_edgelist(G, delimiter=" ", data=True): - """Generate a single line of the graph G in edge list format. - - Parameters - ---------- - G : EasyGraph graph - - delimiter : string, optional - Separator for node labels - - data : bool or list of keys - If False generate no edge data. If True use a dictionary - representation of edge data. If a list of keys use a list of data - values corresponding to the keys. - - Returns - ------- - lines : string - Lines of data in adjlist format. - - Examples - -------- - >>> G = eg.lollipop_graph(4, 3) - >>> G[1][2]["weight"] = 3 - >>> G[3][4]["capacity"] = 12 - >>> for line in eg.generate_edgelist(G, data=False): - ... print(line) - 0 1 - 0 2 - 0 3 - 1 2 - 1 3 - 2 3 - 3 4 - 4 5 - 5 6 - - >>> for line in eg.generate_edgelist(G): - ... print(line) - 0 1 {} - 0 2 {} - 0 3 {} - 1 2 {'weight': 3} - 1 3 {} - 2 3 {} - 3 4 {'capacity': 12} - 4 5 {} - 5 6 {} - - >>> for line in eg.generate_edgelist(G, data=["weight"]): - ... print(line) - 0 1 - 0 2 - 0 3 - 1 2 3 - 1 3 - 2 3 - 3 4 - 4 5 - 5 6 - - See Also - -------- - write_adjlist, read_adjlist - """ - edges = G.edges - if edges and len(edges[0]) > 3: - # multigraph - edges = ((u, v, d) for u, v, _, d in edges) - if data is True: - for u, v, d in edges: - e = u, v, dict(d) - yield delimiter.join(map(str, e)) - elif data is False: - for u, v, _ in edges: - e = u, v - yield delimiter.join(map(str, e)) - else: - for u, v, d in edges: - e = [u, v] - try: - e.extend(d[k] for k in data) - except KeyError: - pass # missing data for this edge, should warn? - yield delimiter.join(map(str, e))
- - -
[docs]@open_file(1, mode="wb") -def write_edgelist(G, path, comments="#", delimiter=" ", data=True, encoding="utf-8"): - """Write graph as a list of edges. - - Parameters - ---------- - G : graph - A EasyGraph graph - path : file or string - File or filename to write. If a file is provided, it must be - opened in 'wb' mode. Filenames ending in .gz or .bz2 will be compressed. - comments : string, optional - The character used to indicate the start of a comment - delimiter : string, optional - The string used to separate values. The default is whitespace. - data : bool or list, optional - If False write no edge data. - If True write a string representation of the edge data dictionary.. - If a list (or other iterable) is provided, write the keys specified - in the list. - encoding: string, optional - Specify which encoding to use when writing file. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> eg.write_edgelist(G, "test.edgelist") - >>> G = eg.path_graph(4) - >>> fh = open("test.edgelist", "wb") - >>> eg.write_edgelist(G, fh) - >>> eg.write_edgelist(G, "test.edgelist.gz") - >>> eg.write_edgelist(G, "test.edgelist.gz", data=False) - - >>> G = eg.Graph() - >>> G.add_edge(1, 2, weight=7, color="red") - >>> eg.write_edgelist(G, "test.edgelist", data=False) - >>> eg.write_edgelist(G, "test.edgelist", data=["color"]) - >>> eg.write_edgelist(G, "test.edgelist", data=["color", "weight"]) - - See Also - -------- - read_edgelist - write_weighted_edgelist - """ - - for line in generate_edgelist(G, delimiter, data): - line += "\n" - path.write(line.encode(encoding))
- - -
[docs]@open_file(0, mode="rb") -def read_edgelist( - path, - comments="#", - delimiter=None, - create_using=None, - nodetype=None, - data=True, - edgetype=None, - encoding="utf-8", -): - """Read a graph from a list of edges. - - Parameters - ---------- - path : file or string - File or filename to read. If a file is provided, it must be - opened in 'rb' mode. - Filenames ending in .gz or .bz2 will be uncompressed. - comments : string, optional - The character used to indicate the start of a comment. To specify that - no character should be treated as a comment, use ``comments=None``. - delimiter : string, optional - The string used to separate values. The default is whitespace. - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - nodetype : int, float, str, Python type, optional - Convert node data from strings to specified type - data : bool or list of (label,type) tuples - Tuples specifying dictionary key names and types for edge data - edgetype : int, float, str, Python type, optional OBSOLETE - Convert edge data from strings to specified type and use as 'weight' - encoding: string, optional - Specify which encoding to use when reading file. - - Returns - ------- - G : graph - A easygraph Graph or other type specified with create_using - - Examples - -------- - >>> eg.write_edgelist(eg.path_graph(4), "test.edgelist") - >>> G = eg.read_edgelist("test.edgelist") - - >>> fh = open("test.edgelist", "rb") - >>> G = eg.read_edgelist(fh) - >>> fh.close() - - >>> G = eg.read_edgelist("test.edgelist", nodetype=int) - >>> G = eg.read_edgelist("test.edgelist", create_using=eg.DiGraph) - - Edgelist with data in a list: - - >>> textline = "1 2 3" - >>> fh = open("test.edgelist", "w") - >>> d = fh.write(textline) - >>> fh.close() - >>> G = eg.read_edgelist("test.edgelist", nodetype=int, data=(("weight", float),)) - >>> list(G) - [1, 2] - >>> list(G.edges) - [(1, 2, {'weight': 3.0})] - - See parse_edgelist() for more examples of formatting. - - See Also - -------- - parse_edgelist - write_edgelist - - Notes - ----- - Since nodes must be hashable, the function nodetype must return hashable - types (e.g. int, float, str, frozenset - or tuples of those, etc.) - """ - lines = (line if isinstance(line, str) else line.decode(encoding) for line in path) - return parse_edgelist( - lines, - comments=comments, - delimiter=delimiter, - create_using=create_using, - nodetype=nodetype, - data=data, - )
- - -
[docs]def write_weighted_edgelist(G, path, comments="#", delimiter=" ", encoding="utf-8"): - """Write graph G as a list of edges with numeric weights. - - Parameters - ---------- - G : graph - A EasyGraph graph - path : file or string - File or filename to write. If a file is provided, it must be - opened in 'wb' mode. - Filenames ending in .gz or .bz2 will be compressed. - comments : string, optional - The character used to indicate the start of a comment - delimiter : string, optional - The string used to separate values. The default is whitespace. - encoding: string, optional - Specify which encoding to use when writing file. - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_edge(1, 2, weight=7) - >>> eg.write_weighted_edgelist(G, "test.weighted.edgelist") - - See Also - -------- - read_edgelist - write_edgelist - read_weighted_edgelist - """ - write_edgelist( - G, - path, - comments=comments, - delimiter=delimiter, - data=("weight",), - encoding=encoding, - )
- - -
[docs]def read_weighted_edgelist( - path, - comments="#", - delimiter=None, - create_using=None, - nodetype=None, - encoding="utf-8", -): - """Read a graph as list of edges with numeric weights. - - Parameters - ---------- - path : file or string - File or filename to read. If a file is provided, it must be - opened in 'rb' mode. - Filenames ending in .gz or .bz2 will be uncompressed. - comments : string, optional - The character used to indicate the start of a comment. - delimiter : string, optional - The string used to separate values. The default is whitespace. - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - nodetype : int, float, str, Python type, optional - Convert node data from strings to specified type - encoding: string, optional - Specify which encoding to use when reading file. - - Returns - ------- - G : graph - A easygraph Graph or other type specified with create_using - - Notes - ----- - Since nodes must be hashable, the function nodetype must return hashable - types (e.g. int, float, str, frozenset - or tuples of those, etc.) - - Example edgelist file format. - - With numeric edge data:: - - # read with - # >>> G=eg.read_weighted_edgelist(fh) - # source target data - a b 1 - a c 3.14159 - d e 42 - - See Also - -------- - write_weighted_edgelist - """ - return read_edgelist( - path, - comments=comments, - delimiter=delimiter, - create_using=create_using, - nodetype=nodetype, - data=(("weight", float),), - encoding=encoding, - )
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/gexf.html b/docs/_modules/easygraph/readwrite/gexf.html deleted file mode 100644 index 65e55f75..00000000 --- a/docs/_modules/easygraph/readwrite/gexf.html +++ /dev/null @@ -1,1130 +0,0 @@ - - - - - - easygraph.readwrite.gexf — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.readwrite.gexf

-import itertools
-import time
-
-from xml.etree.ElementTree import Element
-from xml.etree.ElementTree import ElementTree
-from xml.etree.ElementTree import SubElement
-from xml.etree.ElementTree import register_namespace
-from xml.etree.ElementTree import tostring
-
-import easygraph as eg
-
-from easygraph.utils import *
-
-
-__all__ = ["write_gexf", "relabel_gexf_graph", "generate_gexf", "read_gexf"]
-
-
-
[docs]def write_gexf(G, path, encoding="utf-8", prettyprint=True, version="1.2draft"): - """Write G in GEXF format to path. - - "GEXF (Graph Exchange XML Format) is a language for describing - complex networks structures, their associated data and dynamics" [1]_. - - Node attributes are checked according to the version of the GEXF - schemas used for parameters which are not user defined, - e.g. visualization 'viz' [2]_. See example for usage. - - Parameters - ---------- - G : graph - An EasyGraph graph - path : file or string - File or file name to write. - File names ending in .gz or .bz2 will be compressed. - encoding : string (optional, default: 'utf-8') - Encoding for text data. - prettyprint : bool (optional, default: True) - If True use line breaks and indenting in output XML. - version: string (optional, default: '1.2draft') - The version of GEXF to be used for nodes attributes checking - - Examples - -------- - >>> G = eg.path_graph(4) - >>> eg.write_gexf(G, "test.gexf") - - """ - writer = GEXFWriter(encoding=encoding, prettyprint=prettyprint, version=version) - writer.add_graph(G) - writer.write(path)
- - -
[docs]def generate_gexf(G, encoding="utf-8", prettyprint=True, version="1.2draft"): - """Generate lines of GEXF format representation of G. - - "GEXF (Graph Exchange XML Format) is a language for describing - complex networks structures, their associated data and dynamics" [1]_. - - Parameters - ---------- - G : graph - A EasyGraph graph - encoding : string (optional, default: 'utf-8') - Encoding for text data. - prettyprint : bool (optional, default: True) - If True use line breaks and indenting in output XML. - version : string (default: 1.2draft) - Version of GEFX File Format (see http://gexf.net/schema.html) - Supported values: "1.1draft", "1.2draft" - - - Examples - -------- - >>> G = eg.path_graph(4) - >>> linefeed = chr(10) # linefeed=\n - >>> s = linefeed.join(eg.generate_gexf(G)) - >>> for line in eg.generate_gexf(G): # doctest: +SKIP - ... print(line) - - Notes - ----- - This implementation does not support mixed graphs (directed and undirected - edges together). - - The node id attribute is set to be the string of the node label. - If you want to specify an id use set it as node data, e.g. - node['a']['id']=1 to set the id of node 'a' to 1. - - References - ---------- - .. [1] GEXF File Format, https://gephi.org/gexf/format/ - """ - writer = GEXFWriter(encoding=encoding, prettyprint=prettyprint, version=version) - writer.add_graph(G) - yield from str(writer).splitlines()
- - -
[docs]@open_file(0, mode="rb") -def read_gexf(path, node_type=None, relabel=False, version="1.2draft"): - """Read graph in GEXF format from path. - - "GEXF (Graph Exchange XML Format) is a language for describing - complex networks structures, their associated data and dynamics" [1]_. - - Parameters - ---------- - path : file or string - File or file name to read. - File names ending in .gz or .bz2 will be decompressed. - node_type: Python type (default: None) - Convert node ids to this type if not None. - relabel : bool (default: False) - If True relabel the nodes to use the GEXF node "label" attribute - instead of the node "id" attribute as the EasyGraph node label. - version : string (default: 1.2draft) - Version of GEFX File Format (see http://gexf.net/schema.html) - Supported values: "1.1draft", "1.2draft" - - Returns - ------- - graph: EasyGraph graph - If no parallel edges are found a Graph or DiGraph is returned. - Otherwise a MultiGraph or MultiDiGraph is returned. - - Notes - ----- - This implementation does not support mixed graphs (directed and undirected - edges together). - - References - ---------- - .. [1] GEXF File Format, http://gexf.net/ - """ - reader = GEXFReader(node_type=node_type, version=version) - if relabel: - G = relabel_gexf_graph(reader(path)) - else: - G = reader(path) - return G
- - -class GEXF: - versions = {} - d = { - "NS_GEXF": "http://www.gexf.net/1.1draft", - "NS_VIZ": "http://www.gexf.net/1.1draft/viz", - "NS_XSI": "http://www.w3.org/2001/XMLSchema-instance", - "SCHEMALOCATION": " ".join( - ["http://www.gexf.net/1.1draft", "http://www.gexf.net/1.1draft/gexf.xsd"] - ), - "VERSION": "1.1", - } - versions["1.1draft"] = d - d = { - "NS_GEXF": "http://www.gexf.net/1.2draft", - "NS_VIZ": "http://www.gexf.net/1.2draft/viz", - "NS_XSI": "http://www.w3.org/2001/XMLSchema-instance", - "SCHEMALOCATION": " ".join( - ["http://www.gexf.net/1.2draft", "http://www.gexf.net/1.2draft/gexf.xsd"] - ), - "VERSION": "1.2", - } - versions["1.2draft"] = d - - def construct_types(self): - types = [ - (int, "integer"), - (float, "float"), - (float, "double"), - (bool, "boolean"), - (list, "string"), - (dict, "string"), - (int, "long"), - (str, "liststring"), - (str, "anyURI"), - (str, "string"), - ] - - # These additions to types allow writing numpy types - try: - import numpy as np - except ImportError: - pass - else: - # prepend so that python types are created upon read (last entry wins) - types = [ - (np.float64, "float"), - (np.float32, "float"), - (np.float16, "float"), - (np.float_, "float"), - (np.int_, "int"), - (np.int8, "int"), - (np.int16, "int"), - (np.int32, "int"), - (np.int64, "int"), - (np.uint8, "int"), - (np.uint16, "int"), - (np.uint32, "int"), - (np.uint64, "int"), - (np.int_, "int"), - (np.intc, "int"), - (np.intp, "int"), - ] + types - - self.xml_type = dict(types) - self.python_type = dict(reversed(a) for a in types) - - # http://www.w3.org/TR/xmlschema-2/#boolean - convert_bool = { - "true": True, - "false": False, - "True": True, - "False": False, - "0": False, - 0: False, - "1": True, - 1: True, - } - - def set_version(self, version): - d = self.versions.get(version) - if d is None: - raise AssertionError(f"Unknown GEXF version {version}.") - self.NS_GEXF = d["NS_GEXF"] - self.NS_VIZ = d["NS_VIZ"] - self.NS_XSI = d["NS_XSI"] - self.SCHEMALOCATION = d["SCHEMALOCATION"] - self.VERSION = d["VERSION"] - self.version = version - - -class GEXFWriter(GEXF): - # class for writing GEXF format files - # use write_gexf() function - def __init__( - self, graph=None, encoding="utf-8", prettyprint=True, version="1.2draft" - ): - self.construct_types() - self.prettyprint = prettyprint - self.encoding = encoding - self.set_version(version) - self.xml = Element( - "gexf", - { - "xmlns": self.NS_GEXF, - "xmlns:xsi": self.NS_XSI, - "xsi:schemaLocation": self.SCHEMALOCATION, - "version": self.VERSION, - }, - ) - - # Make meta element a non-graph element - # Also add lastmodifieddate as attribute, not tag - meta_element = Element("meta") - subelement_text = f"EasyGraph" - SubElement(meta_element, "creator").text = subelement_text - meta_element.set("lastmodifieddate", time.strftime("%Y-%m-%d")) - self.xml.append(meta_element) - - register_namespace("viz", self.NS_VIZ) - - # counters for edge and attribute identifiers - self.edge_id = itertools.count() - self.attr_id = itertools.count() - self.all_edge_ids = set() - # default attributes are stored in dictionaries - self.attr = {} - self.attr["node"] = {} - self.attr["edge"] = {} - self.attr["node"]["dynamic"] = {} - self.attr["node"]["static"] = {} - self.attr["edge"]["dynamic"] = {} - self.attr["edge"]["static"] = {} - - if graph is not None: - self.add_graph(graph) - - def __str__(self): - if self.prettyprint: - self.indent(self.xml) - s = tostring(self.xml).decode(self.encoding) - return s - - def add_graph(self, G): - # first pass through G collecting edge ids - for u, v, dd in G.edges: - eid = dd.get("id") - if eid is not None: - self.all_edge_ids.add(str(eid)) - # set graph attributes - if G.graph.get("mode") == "dynamic": - mode = "dynamic" - else: - mode = "static" - # Add a graph element to the XML - if G.is_directed(): - default = "directed" - else: - default = "undirected" - name = G.graph.get("name", "") - graph_element = Element("graph", defaultedgetype=default, mode=mode, name=name) - self.graph_element = graph_element - self.add_nodes(G, graph_element) - self.add_edges(G, graph_element) - self.xml.append(graph_element) - - def add_nodes(self, G, graph_element): - nodes_element = Element("nodes") - for node, data in G.nodes.items(): - node_data = data.copy() - node_id = str(node_data.pop("id", node)) - kw = {"id": node_id} - label = str(node_data.pop("label", node)) - kw["label"] = label - try: - pid = node_data.pop("pid") - kw["pid"] = str(pid) - except KeyError: - pass - try: - start = node_data.pop("start") - kw["start"] = str(start) - self.alter_graph_mode_timeformat(start) - except KeyError: - pass - try: - end = node_data.pop("end") - kw["end"] = str(end) - self.alter_graph_mode_timeformat(end) - except KeyError: - pass - # add node element with attributes - node_element = Element("node", **kw) - # add node element and attr subelements - default = G.graph.get("node_default", {}) - node_data = self.add_parents(node_element, node_data) - if self.VERSION == "1.1": - node_data = self.add_slices(node_element, node_data) - else: - node_data = self.add_spells(node_element, node_data) - node_data = self.add_viz(node_element, node_data) - node_data = self.add_attributes("node", node_element, node_data, default) - nodes_element.append(node_element) - graph_element.append(nodes_element) - - def get_attr_id(self, title, attr_type, edge_or_node, default, mode): - # find the id of the attribute or generate a new id - try: - return self.attr[edge_or_node][mode][title] - except KeyError: - # generate new id - new_id = str(next(self.attr_id)) - self.attr[edge_or_node][mode][title] = new_id - attr_kwargs = {"id": new_id, "title": title, "type": attr_type} - attribute = Element("attribute", **attr_kwargs) - # add subelement for data default value if present - default_title = default.get(title) - if default_title is not None: - default_element = Element("default") - default_element.text = str(default_title) - attribute.append(default_element) - # new insert it into the XML - attributes_element = None - for a in self.graph_element.findall("attributes"): - # find existing attributes element by class and mode - a_class = a.get("class") - a_mode = a.get("mode", "static") - if a_class == edge_or_node and a_mode == mode: - attributes_element = a - if attributes_element is None: - # create new attributes element - attr_kwargs = {"mode": mode, "class": edge_or_node} - attributes_element = Element("attributes", **attr_kwargs) - self.graph_element.insert(0, attributes_element) - attributes_element.append(attribute) - return new_id - - def add_edges(self, G, graph_element): - def edge_key_data(G): - if G.is_multigraph(): - for u, v, key, data in G.edges: - edge_data = data.copy() - edge_data.update(key=key) - edge_id = edge_data.pop("id", None) - if edge_id is None: - edge_id = next(self.edge_id) - while str(edge_id) in self.all_edge_ids: - edge_id = next(self.edge_id) - self.all_edge_ids.add(str(edge_id)) - yield u, v, edge_id, edge_data - else: - for u, v, data in G.edges: - edge_data = data.copy() - edge_id = edge_data.pop("id", None) - if edge_id is None: - edge_id = next(self.edge_id) - while str(edge_id) in self.all_edge_ids: - edge_id = next(self.edge_id) - self.all_edge_ids.add(str(edge_id)) - yield u, v, edge_id, edge_data - - edges_element = Element("edges") - for u, v, key, edge_data in edge_key_data(G): - kw = {"id": str(key)} - try: - edge_label = edge_data.pop("label") - kw["label"] = str(edge_label) - except KeyError: - pass - try: - edge_weight = edge_data.pop("weight") - kw["weight"] = str(edge_weight) - except KeyError: - pass - try: - edge_type = edge_data.pop("type") - kw["type"] = str(edge_type) - except KeyError: - pass - try: - start = edge_data.pop("start") - kw["start"] = str(start) - self.alter_graph_mode_timeformat(start) - except KeyError: - pass - try: - end = edge_data.pop("end") - kw["end"] = str(end) - self.alter_graph_mode_timeformat(end) - except KeyError: - pass - source_id = str(G.nodes[u].get("id", u)) - target_id = str(G.nodes[v].get("id", v)) - edge_element = Element("edge", source=source_id, target=target_id, **kw) - default = G.graph.get("edge_default", {}) - if self.VERSION == "1.1": - edge_data = self.add_slices(edge_element, edge_data) - else: - edge_data = self.add_spells(edge_element, edge_data) - edge_data = self.add_viz(edge_element, edge_data) - edge_data = self.add_attributes("edge", edge_element, edge_data, default) - edges_element.append(edge_element) - graph_element.append(edges_element) - - def add_attributes(self, node_or_edge, xml_obj, data, default): - # Add attrvalues to node or edge - attvalues = Element("attvalues") - if len(data) == 0: - return data - mode = "static" - for k, v in data.items(): - # rename generic multigraph key to avoid any name conflict - if k == "key": - k = "easygraph_key" - val_type = type(v) - if val_type not in self.xml_type: - raise TypeError(f"attribute value type is not allowed: {val_type}") - if isinstance(v, list): - # dynamic data - for val, start, end in v: - val_type = type(val) - if start is not None or end is not None: - mode = "dynamic" - self.alter_graph_mode_timeformat(start) - self.alter_graph_mode_timeformat(end) - break - attr_id = self.get_attr_id( - str(k), self.xml_type[val_type], node_or_edge, default, mode - ) - for val, start, end in v: - e = Element("attvalue") - e.attrib["for"] = attr_id - e.attrib["value"] = str(val) - # Handle nan, inf, -inf differently - if val_type == float: - if e.attrib["value"] == "inf": - e.attrib["value"] = "INF" - elif e.attrib["value"] == "nan": - e.attrib["value"] = "NaN" - elif e.attrib["value"] == "-inf": - e.attrib["value"] = "-INF" - if start is not None: - e.attrib["start"] = str(start) - if end is not None: - e.attrib["end"] = str(end) - attvalues.append(e) - else: - # static data - mode = "static" - attr_id = self.get_attr_id( - str(k), self.xml_type[val_type], node_or_edge, default, mode - ) - e = Element("attvalue") - e.attrib["for"] = attr_id - if isinstance(v, bool): - e.attrib["value"] = str(v).lower() - else: - e.attrib["value"] = str(v) - # Handle float nan, inf, -inf differently - if val_type == float: - if e.attrib["value"] == "inf": - e.attrib["value"] = "INF" - elif e.attrib["value"] == "nan": - e.attrib["value"] = "NaN" - elif e.attrib["value"] == "-inf": - e.attrib["value"] = "-INF" - attvalues.append(e) - xml_obj.append(attvalues) - return data - - def add_viz(self, element, node_data): - viz = node_data.pop("viz", False) - if viz: - color = viz.get("color") - if color is not None: - if self.VERSION == "1.1": - e = Element( - f"{{{self.NS_VIZ}}}color", - r=str(color.get("r")), - g=str(color.get("g")), - b=str(color.get("b")), - ) - else: - e = Element( - f"{{{self.NS_VIZ}}}color", - r=str(color.get("r")), - g=str(color.get("g")), - b=str(color.get("b")), - a=str(color.get("a")), - ) - element.append(e) - - size = viz.get("size") - if size is not None: - e = Element(f"{{{self.NS_VIZ}}}size", value=str(size)) - element.append(e) - - thickness = viz.get("thickness") - if thickness is not None: - e = Element(f"{{{self.NS_VIZ}}}thickness", value=str(thickness)) - element.append(e) - - shape = viz.get("shape") - if shape is not None: - if shape.startswith("http"): - e = Element( - f"{{{self.NS_VIZ}}}shape", value="image", uri=str(shape) - ) - else: - e = Element(f"{{{self.NS_VIZ}}}shape", value=str(shape)) - element.append(e) - - position = viz.get("position") - if position is not None: - e = Element( - f"{{{self.NS_VIZ}}}position", - x=str(position.get("x")), - y=str(position.get("y")), - z=str(position.get("z")), - ) - element.append(e) - return node_data - - def add_parents(self, node_element, node_data): - parents = node_data.pop("parents", False) - if parents: - parents_element = Element("parents") - for p in parents: - e = Element("parent") - e.attrib["for"] = str(p) - parents_element.append(e) - node_element.append(parents_element) - return node_data - - def add_slices(self, node_or_edge_element, node_or_edge_data): - slices = node_or_edge_data.pop("slices", False) - if slices: - slices_element = Element("slices") - for start, end in slices: - e = Element("slice", start=str(start), end=str(end)) - slices_element.append(e) - node_or_edge_element.append(slices_element) - return node_or_edge_data - - def add_spells(self, node_or_edge_element, node_or_edge_data): - spells = node_or_edge_data.pop("spells", False) - if spells: - spells_element = Element("spells") - for start, end in spells: - e = Element("spell") - if start is not None: - e.attrib["start"] = str(start) - self.alter_graph_mode_timeformat(start) - if end is not None: - e.attrib["end"] = str(end) - self.alter_graph_mode_timeformat(end) - spells_element.append(e) - node_or_edge_element.append(spells_element) - return node_or_edge_data - - def alter_graph_mode_timeformat(self, start_or_end): - if self.graph_element.get("mode") == "static": - if start_or_end is not None: - if isinstance(start_or_end, str): - timeformat = "date" - elif isinstance(start_or_end, float): - timeformat = "double" - elif isinstance(start_or_end, int): - timeformat = "long" - else: - raise AssertionError( - "timeformat should be of the type int, float or str" - ) - self.graph_element.set("timeformat", timeformat) - self.graph_element.set("mode", "dynamic") - - def write(self, fh): - if self.prettyprint: - self.indent(self.xml) - document = ElementTree(self.xml) - document.write(fh, encoding=self.encoding, xml_declaration=True) - - def indent(self, elem, level=0): - i = "\n" + " " * level - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - self.indent(elem, level + 1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - - -class GEXFReader(GEXF): - # Class to read GEXF format files - # use read_gexf() function - def __init__(self, node_type=None, version="1.2draft"): - self.construct_types() - self.node_type = node_type - # assume simple graph and test for multigraph on read - self.simple_graph = True - self.set_version(version) - - def __call__(self, stream): - self.xml = ElementTree(file=stream) - g = self.xml.find(f"{{{self.NS_GEXF}}}graph") - if g is not None: - return self.make_graph(g) - # try all the versions - for version in self.versions: - self.set_version(version) - g = self.xml.find(f"{{{self.NS_GEXF}}}graph") - if g is not None: - return self.make_graph(g) - raise EasyGraphError("No <graph> element in GEXF file.") - - def make_graph(self, graph_xml): - edgedefault = graph_xml.get("defaultedgetype", None) - if edgedefault == "directed": - G = eg.MultiDiGraph() - else: - G = eg.MultiGraph() - - # graph attributes - graph_name = graph_xml.get("name", "") - if graph_name != "": - G.graph["name"] = graph_name - graph_start = graph_xml.get("start") - if graph_start is not None: - G.graph["start"] = graph_start - graph_end = graph_xml.get("end") - if graph_end is not None: - G.graph["end"] = graph_end - graph_mode = graph_xml.get("mode", "") - if graph_mode == "dynamic": - G.graph["mode"] = "dynamic" - else: - G.graph["mode"] = "static" - - # timeformat - self.timeformat = graph_xml.get("timeformat") - if self.timeformat == "date": - self.timeformat = "string" - - # node and edge attributes - attributes_elements = graph_xml.findall(f"{{{self.NS_GEXF}}}attributes") - # dictionaries to hold attributes and attribute defaults - node_attr = {} - node_default = {} - edge_attr = {} - edge_default = {} - for a in attributes_elements: - attr_class = a.get("class") - if attr_class == "node": - na, nd = self.find_gexf_attributes(a) - node_attr.update(na) - node_default.update(nd) - G.graph["node_default"] = node_default - elif attr_class == "edge": - ea, ed = self.find_gexf_attributes(a) - edge_attr.update(ea) - edge_default.update(ed) - G.graph["edge_default"] = edge_default - else: - raise # unknown attribute class - - # Hack to handle Gephi0.7beta bug - # add weight attribute - ea = {"weight": {"type": "double", "mode": "static", "title": "weight"}} - ed = {} - edge_attr.update(ea) - edge_default.update(ed) - G.graph["edge_default"] = edge_default - - # add nodes - nodes_element = graph_xml.find(f"{{{self.NS_GEXF}}}nodes") - if nodes_element is not None: - for node_xml in nodes_element.findall(f"{{{self.NS_GEXF}}}node"): - self.add_node(G, node_xml, node_attr) - - # add edges - edges_element = graph_xml.find(f"{{{self.NS_GEXF}}}edges") - if edges_element is not None: - for edge_xml in edges_element.findall(f"{{{self.NS_GEXF}}}edge"): - self.add_edge(G, edge_xml, edge_attr) - - # switch to Graph or DiGraph if no parallel edges were found. - if self.simple_graph: - if G.is_directed(): - G = eg.DiGraph(G) - else: - G = eg.Graph(G) - return G - - def add_node(self, G, node_xml, node_attr, node_pid=None): - # add a single node with attributes to the graph - - # get attributes and subattributues for node - data = self.decode_attr_elements(node_attr, node_xml) - data = self.add_parents(data, node_xml) # add any parents - if self.VERSION == "1.1": - data = self.add_slices(data, node_xml) # add slices - else: - data = self.add_spells(data, node_xml) # add spells - data = self.add_viz(data, node_xml) # add viz - data = self.add_start_end(data, node_xml) # add start/end - - # find the node id and cast it to the appropriate type - node_id = node_xml.get("id") - if self.node_type is not None: - node_id = self.node_type(node_id) - - # every node should have a label - node_label = node_xml.get("label") - data["label"] = node_label - - # parent node id - node_pid = node_xml.get("pid", node_pid) - if node_pid is not None: - data["pid"] = node_pid - - # check for subnodes, recursive - subnodes = node_xml.find(f"{{{self.NS_GEXF}}}nodes") - if subnodes is not None: - for node_xml in subnodes.findall(f"{{{self.NS_GEXF}}}node"): - self.add_node(G, node_xml, node_attr, node_pid=node_id) - - G.add_node(node_id, **data) - - def add_start_end(self, data, xml): - # start and end times - ttype = self.timeformat - node_start = xml.get("start") - if node_start is not None: - data["start"] = self.python_type[ttype](node_start) - node_end = xml.get("end") - if node_end is not None: - data["end"] = self.python_type[ttype](node_end) - return data - - def add_viz(self, data, node_xml): - # add viz element for node - viz = {} - color = node_xml.find(f"{{{self.NS_VIZ}}}color") - if color is not None: - if self.VERSION == "1.1": - viz["color"] = { - "r": int(color.get("r")), - "g": int(color.get("g")), - "b": int(color.get("b")), - } - else: - viz["color"] = { - "r": int(color.get("r")), - "g": int(color.get("g")), - "b": int(color.get("b")), - "a": float(color.get("a", 1)), - } - - size = node_xml.find(f"{{{self.NS_VIZ}}}size") - if size is not None: - viz["size"] = float(size.get("value")) - - thickness = node_xml.find(f"{{{self.NS_VIZ}}}thickness") - if thickness is not None: - viz["thickness"] = float(thickness.get("value")) - - shape = node_xml.find(f"{{{self.NS_VIZ}}}shape") - if shape is not None: - viz["shape"] = shape.get("shape") - if viz["shape"] == "image": - viz["shape"] = shape.get("uri") - - position = node_xml.find(f"{{{self.NS_VIZ}}}position") - if position is not None: - viz["position"] = { - "x": float(position.get("x", 0)), - "y": float(position.get("y", 0)), - "z": float(position.get("z", 0)), - } - - if len(viz) > 0: - data["viz"] = viz - return data - - def add_parents(self, data, node_xml): - parents_element = node_xml.find(f"{{{self.NS_GEXF}}}parents") - if parents_element is not None: - data["parents"] = [] - for p in parents_element.findall(f"{{{self.NS_GEXF}}}parent"): - parent = p.get("for") - data["parents"].append(parent) - return data - - def add_slices(self, data, node_or_edge_xml): - slices_element = node_or_edge_xml.find(f"{{{self.NS_GEXF}}}slices") - if slices_element is not None: - data["slices"] = [] - for s in slices_element.findall(f"{{{self.NS_GEXF}}}slice"): - start = s.get("start") - end = s.get("end") - data["slices"].append((start, end)) - return data - - def add_spells(self, data, node_or_edge_xml): - spells_element = node_or_edge_xml.find(f"{{{self.NS_GEXF}}}spells") - if spells_element is not None: - data["spells"] = [] - ttype = self.timeformat - for s in spells_element.findall(f"{{{self.NS_GEXF}}}spell"): - start = self.python_type[ttype](s.get("start")) - end = self.python_type[ttype](s.get("end")) - data["spells"].append((start, end)) - return data - - def add_edge(self, G, edge_element, edge_attr): - # add an edge to the graph - - # raise error if we find mixed directed and undirected edges - edge_direction = edge_element.get("type") - if G.is_directed() and edge_direction == "undirected": - raise EasyGraphError("Undirected edge found in directed graph.") - if (not G.is_directed()) and edge_direction == "directed": - raise EasyGraphError("Directed edge found in undirected graph.") - - # Get source and target and recast type if required - source = edge_element.get("source") - target = edge_element.get("target") - if self.node_type is not None: - source = self.node_type(source) - target = self.node_type(target) - - data = self.decode_attr_elements(edge_attr, edge_element) - data = self.add_start_end(data, edge_element) - - if self.VERSION == "1.1": - data = self.add_slices(data, edge_element) # add slices - else: - data = self.add_spells(data, edge_element) # add spells - - # GEXF stores edge ids as an attribute - # EasyGraph uses them as keys in multigraphs - # if easygraph_key is not specified as an attribute - edge_id = edge_element.get("id") - if edge_id is not None: - data["id"] = edge_id - - # check if there is a 'multigraph_key' and use that as edge_id - multigraph_key = data.pop("easygraph_key", None) - if multigraph_key is not None: - edge_id = multigraph_key - - weight = edge_element.get("weight") - if weight is not None: - data["weight"] = float(weight) - - edge_label = edge_element.get("label") - if edge_label is not None: - data["label"] = edge_label - - if G.has_edge(source, target): - # seen this edge before - this is a multigraph - self.simple_graph = False - G.add_edge(source, target, key=edge_id, **data) - if edge_direction == "mutual": - G.add_edge(target, source, key=edge_id, **data) - - def decode_attr_elements(self, gexf_keys, obj_xml): - # Use the key information to decode the attr XML - attr = {} - # look for outer '<attvalues>' element - attr_element = obj_xml.find(f"{{{self.NS_GEXF}}}attvalues") - if attr_element is not None: - # loop over <attvalue> elements - for a in attr_element.findall(f"{{{self.NS_GEXF}}}attvalue"): - key = a.get("for") # for is required - try: # should be in our gexf_keys dictionary - title = gexf_keys[key]["title"] - except KeyError as err: - raise eg.EasyGraphError(f"No attribute defined for={key}.") from err - atype = gexf_keys[key]["type"] - value = a.get("value") - if atype == "boolean": - value = self.convert_bool[value] - else: - value = self.python_type[atype](value) - if gexf_keys[key]["mode"] == "dynamic": - # for dynamic graphs use list of three-tuples - # [(value1,start1,end1), (value2,start2,end2), etc] - ttype = self.timeformat - start = self.python_type[ttype](a.get("start")) - end = self.python_type[ttype](a.get("end")) - if title in attr: - attr[title].append((value, start, end)) - else: - attr[title] = [(value, start, end)] - else: - # for static graphs just assign the value - attr[title] = value - return attr - - def find_gexf_attributes(self, attributes_element): - # Extract all the attributes and defaults - attrs = {} - defaults = {} - mode = attributes_element.get("mode") - for k in attributes_element.findall(f"{{{self.NS_GEXF}}}attribute"): - attr_id = k.get("id") - title = k.get("title") - atype = k.get("type") - attrs[attr_id] = {"title": title, "type": atype, "mode": mode} - # check for the 'default' subelement of key element and add - default = k.find(f"{{{self.NS_GEXF}}}default") - if default is not None: - if atype == "boolean": - value = self.convert_bool[default.text] - else: - value = self.python_type[atype](default.text) - defaults[title] = value - return attrs, defaults - - -
[docs]def relabel_gexf_graph(G): - """Relabel graph using "label" node keyword for node label. - - Parameters - ---------- - G : graph - A EasyGraph graph read from GEXF data - - Returns - ------- - H : graph - A EasyGraph graph with relabeled nodes - - Raises - ------ - EasyGraphError - If node labels are missing or not unique while relabel=True. - - Notes - ----- - This function relabels the nodes in a EasyGraph graph with the - "label" attribute. It also handles relabeling the specific GEXF - node attributes "parents", and "pid". - """ - # build mapping of node labels, do some error checking - try: - mapping = [(u, G.nodes[u]["label"]) for u in G] - except KeyError as err: - raise EasyGraphError( - "Failed to relabel nodes: missing node labels found. Use relabel=False." - ) from err - x, y = zip(*mapping) - if len(set(y)) != len(G): - raise EasyGraphError( - "Failed to relabel nodes: duplicate node labels found. Use relabel=False." - ) - mapping = dict(mapping) - H = eg.relabel_nodes(G, mapping) - # relabel attributes - for n in G: - m = mapping[n] - H.nodes[m]["id"] = n - H.nodes[m].pop("label") - if "pid" in H.nodes[m]: - H.nodes[m]["pid"] = mapping[G.nodes[n]["pid"]] - if "parents" in H.nodes[m]: - H.nodes[m]["parents"] = [mapping[p] for p in G.nodes[n]["parents"]] - return H
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/gml.html b/docs/_modules/easygraph/readwrite/gml.html deleted file mode 100644 index 25e2a3fc..00000000 --- a/docs/_modules/easygraph/readwrite/gml.html +++ /dev/null @@ -1,916 +0,0 @@ - - - - - - easygraph.readwrite.gml — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.readwrite.gml

-"""
-Read graphs in GML format.
-
-"GML, the Graph Modelling Language, is our proposal for a portable
-file format for graphs. GML's key features are portability, simple
-syntax, extensibility and flexibility. A GML file consists of a
-hierarchical key-value lists. Graphs can be annotated with arbitrary
-data structures. The idea for a common file format was born at the
-GD'95; this proposal is the outcome of many discussions. GML is the
-standard file format in the Graphlet graph editor system. It has been
-overtaken and adapted by several other systems for drawing graphs."
-
-GML files are stored using a 7-bit ASCII encoding with any extended
-ASCII characters (iso8859-1) appearing as HTML character entities.
-You will need to give some thought into how the exported data should
-interact with different languages and even different Python versions.
-Re-importing from gml is also a concern.
-
-Without specifying a `stringizer`/`destringizer`, the code is capable of
-writing `int`/`float`/`str`/`dict`/`list` data as required by the GML
-specification.  For writing other data types, and for reading data other
-than `str` you need to explicitly supply a `stringizer`/`destringizer`.
-
-For additional documentation on the GML file format, please see the
-`GML website <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_.
-
-Several example graphs in GML format may be found on Mark Newman's
-`Network data page <http://www-personal.umich.edu/~mejn/netdata/>`_.
-"""
-
-
-import html.entities as htmlentitydefs
-import re
-import warnings
-
-from ast import literal_eval
-from collections import defaultdict
-from enum import Enum
-from io import StringIO
-from typing import Any
-from typing import NamedTuple
-from unicodedata import category
-
-import easygraph as eg
-
-from easygraph.utils import open_file
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = ["read_gml", "parse_gml", "generate_gml", "write_gml"]
-
-LIST_START_VALUE = "_easygraph_list_start"
-
-
-def escape(text):
-    """Use XML character references to escape characters.
-
-    Use XML character references for unprintable or non-ASCII
-    characters, double quotes and ampersands in a string
-    """
-
-    def fixup(m):
-        ch = m.group(0)
-        return "&#" + str(ord(ch)) + ";"
-
-    text = re.sub('[^ -~]|[&"]', fixup, text)
-    return text if isinstance(text, str) else str(text)
-
-
-def unescape(text):
-    """Replace XML character references with the referenced characters"""
-
-    def fixup(m):
-        text = m.group(0)
-        if text[1] == "#":
-            # Character reference
-            if text[2] == "x":
-                code = int(text[3:-1], 16)
-            else:
-                code = int(text[2:-1])
-        else:
-            # Named entity
-            try:
-                code = htmlentitydefs.name2codepoint[text[1:-1]]
-            except KeyError:
-                return text  # leave unchanged
-        try:
-            return chr(code)
-        except (ValueError, OverflowError):
-            return text  # leave unchanged
-
-    return re.sub("&(?:[0-9A-Za-z]+|#(?:[0-9]+|x[0-9A-Fa-f]+));", fixup, text)
-
-
-def literal_destringizer(rep):
-    """Convert a Python literal to the value it represents.
-
-    Parameters
-    ----------
-    rep : string
-        A Python literal.
-
-    Returns
-    -------
-    value : object
-        The value of the Python literal.
-
-    Raises
-    ------
-    ValueError
-        If `rep` is not a Python literal.
-    """
-    msg = "literal_destringizer is deprecated and will be removed in 3.0."
-    warnings.warn(msg, DeprecationWarning)
-    if isinstance(rep, str):
-        orig_rep = rep
-        try:
-            return literal_eval(rep)
-        except SyntaxError as err:
-            raise ValueError(f"{orig_rep!r} is not a valid Python literal") from err
-    else:
-        raise ValueError(f"{rep!r} is not a string")
-
-
-class Pattern(Enum):
-    """encodes the index of each token-matching pattern in `tokenize`."""
-
-    KEYS = 0
-    REALS = 1
-    INTS = 2
-    STRINGS = 3
-    DICT_START = 4
-    DICT_END = 5
-    COMMENT_WHITESPACE = 6
-
-
-class Token(NamedTuple):
-    category: Pattern
-    value: Any
-    line: int
-    position: int
-
-
-
[docs]def parse_gml(lines, label="label", destringizer=None): - """Parse GML graph from a string or iterable. - - Parameters - ---------- - lines : string or iterable of strings - Data in GML format. - - label : string, optional - If not None, the parsed nodes will be renamed according to node - attributes indicated by `label`. Default value: 'label'. - - destringizer : callable, optional - A `destringizer` that recovers values stored as strings in GML. If it - cannot convert a string to a value, a `ValueError` is raised. Default - value : None. - - Returns - ------- - G : EasyGraph graph - The parsed graph. - - Raises - ------ - EasyGraphError - If the input cannot be parsed. - - See Also - -------- - write_gml, read_gml - - Notes - ----- - This stores nested GML attributes as dictionaries in the EasyGraph graph, - node, and edge attribute structures. - - GML files are stored using a 7-bit ASCII encoding with any extended - ASCII characters (iso8859-1) appearing as HTML character entities. - Without specifying a `stringizer`/`destringizer`, the code is capable of - writing `int`/`float`/`str`/`dict`/`list` data as required by the GML - specification. For writing other data types, and for reading data other - than `str` you need to explicitly supply a `stringizer`/`destringizer`. - - For additional documentation on the GML file format, please see the - `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. - - See the module docstring :mod:`easygraph.readwrite.gml` for more details. - """ - - def decode_line(line): - if isinstance(line, bytes): - try: - line.decode("ascii") - except UnicodeDecodeError as err: - raise EasyGraphError("input is not ASCII-encoded") from err - if not isinstance(line, str): - line = str(line) - return line - - def filter_lines(lines): - if isinstance(lines, str): - lines = decode_line(lines) - lines = lines.splitlines() - yield from lines - else: - for line in lines: - line = decode_line(line) - if line and line[-1] == "\n": - line = line[:-1] - if line.find("\n") != -1: - raise EasyGraphError("input line contains newline") - yield line - - G = parse_gml_lines(filter_lines(lines), label, destringizer) - return G
- - -def parse_gml_lines(lines, label, destringizer): - """Parse GML `lines` into a graph.""" - - def tokenize(): - patterns = [ - r"[A-Za-z][0-9A-Za-z_]*\b", # keys - # reals - r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+\.[0-9]*|INF)(?:[Ee][+-]?[0-9]+)?", - r"[+-]?[0-9]+", # ints - r'".*?"', # strings - r"\[", # dict start - r"\]", # dict end - r"#.*$|\s+", # comments and whitespaces - ] - tokens = re.compile("|".join(f"({pattern})" for pattern in patterns)) - lineno = 0 - for line in lines: - length = len(line) - pos = 0 - while pos < length: - match = tokens.match(line, pos) - if match is None: - m = f"cannot tokenize {line[pos:]} at ({lineno + 1}, {pos + 1})" - raise EasyGraphError(m) - for i in range(len(patterns)): - group = match.group(i + 1) - if group is not None: - if i == 0: # keys - value = group.rstrip() - elif i == 1: # reals - value = float(group) - elif i == 2: # ints - value = int(group) - else: - value = group - if i != 6: # comments and whitespaces - yield Token(Pattern(i), value, lineno + 1, pos + 1) - pos += len(group) - break - lineno += 1 - yield Token(None, None, lineno + 1, 1) # EOF - - def unexpected(curr_token, expected): - category, value, lineno, pos = curr_token - value = repr(value) if value is not None else "EOF" - raise EasyGraphError(f"expected {expected}, found {value} at ({lineno}, {pos})") - - def consume(curr_token, category, expected): - if curr_token.category == category: - return next(tokens) - unexpected(curr_token, expected) - - def parse_dict(curr_token): - # dict start - curr_token = consume(curr_token, Pattern.DICT_START, "'['") - # dict contents - curr_token, dct = parse_kv(curr_token) - # dict end - curr_token = consume(curr_token, Pattern.DICT_END, "']'") - return curr_token, dct - - def parse_kv(curr_token): - dct = defaultdict(list) - while curr_token.category == Pattern.KEYS: - key = curr_token.value - curr_token = next(tokens) - category = curr_token.category - if category == Pattern.REALS or category == Pattern.INTS: - value = curr_token.value - curr_token = next(tokens) - elif category == Pattern.STRINGS: - value = unescape(curr_token.value[1:-1]) - if destringizer: - try: - value = destringizer(value) - except ValueError: - pass - curr_token = next(tokens) - elif category == Pattern.DICT_START: - curr_token, value = parse_dict(curr_token) - else: - if key in ("id", "label", "source", "target"): - try: - # String convert the token value - value = unescape(str(curr_token.value)) - if destringizer: - try: - value = destringizer(value) - except ValueError: - pass - curr_token = next(tokens) - except Exception: - msg = ( - "an int, float, string, '[' or string" - + " convertible ASCII value for node id or label" - ) - unexpected(curr_token, msg) - elif curr_token.value in {"NAN", "INF"}: - value = float(curr_token.value) - curr_token = next(tokens) - else: # Otherwise error out - unexpected(curr_token, "an int, float, string or '['") - dct[key].append(value) - - def clean_dict_value(value): - if not isinstance(value, list): - return value - if len(value) == 1: - return value[0] - if value[0] == LIST_START_VALUE: - return value[1:] - return value - - dct = {key: clean_dict_value(value) for key, value in dct.items()} - return curr_token, dct - - def parse_graph(): - curr_token, dct = parse_kv(next(tokens)) - if curr_token.category is not None: # EOF - unexpected(curr_token, "EOF") - if "graph" not in dct: - raise EasyGraphError("input contains no graph") - graph = dct["graph"] - if isinstance(graph, list): - raise EasyGraphError("input contains more than one graph") - return graph - - tokens = tokenize() - graph = parse_graph() - directed = graph.pop("directed", False) - multigraph = graph.pop("multigraph", False) - if not multigraph: - G = eg.DiGraph() if directed else eg.Graph() - else: - G = eg.MultiDiGraph() if directed else eg.MultiGraph() - graph_attr = {k: v for k, v in graph.items() if k not in ("node", "edge")} - G.graph.update(graph_attr) - - def pop_attr(dct, category, attr, i): - try: - return dct.pop(attr) - except KeyError as err: - raise EasyGraphError(f"{category} #{i} has no {attr!r} attribute") from err - - nodes = graph.get("node", []) - mapping = {} - node_labels = set() - for i, node in enumerate(nodes if isinstance(nodes, list) else [nodes]): - id = pop_attr(node, "node", "id", i) - if id in G: - raise EasyGraphError(f"node id {id!r} is duplicated") - if label is not None and label != "id": - node_label = pop_attr(node, "node", label, i) - if node_label in node_labels: - raise EasyGraphError(f"node label {node_label!r} is duplicated") - node_labels.add(node_label) - mapping[id] = node_label - G.add_node(id, **node) - - edges = graph.get("edge", []) - for i, edge in enumerate(edges if isinstance(edges, list) else [edges]): - source = pop_attr(edge, "edge", "source", i) - target = pop_attr(edge, "edge", "target", i) - if source not in G: - raise EasyGraphError(f"edge #{i} has undefined source {source!r}") - if target not in G: - raise EasyGraphError(f"edge #{i} has undefined target {target!r}") - if not multigraph: - if not G.has_edge(source, target): - G.add_edge(source, target, **edge) - else: - arrow = "->" if directed else "--" - msg = f"edge #{i} ({source!r}{arrow}{target!r}) is duplicated" - raise EasyGraphError(msg) - else: - key = edge.pop("key", None) - if key is not None and G.has_edge(source, target, key): - arrow = "->" if directed else "--" - msg = f"edge #{i} ({source!r}{arrow}{target!r}, {key!r})" - msg2 = 'Hint: If multigraph add "multigraph 1" to file header.' - raise EasyGraphError(msg + " is duplicated\n" + msg2) - G.add_edge(source, target, key, **edge) - - if label is not None and label != "id": - G = eg.relabel_nodes(G, mapping) - return G - - -
[docs]def generate_gml(G, stringizer=None): - r"""Generate a single entry of the graph `G` in GML format. - - Parameters - ---------- - G : EasyGraph graph - The graph to be converted to GML. - - stringizer : callable, optional - A `stringizer` which converts non-int/non-float/non-dict values into - strings. If it cannot convert a value into a string, it should raise a - `ValueError` to indicate that. Default value: None. - - Returns - ------- - lines: generator of strings - Lines of GML data. Newlines are not appended. - - Raises - ------ - EasyGraphError - If `stringizer` cannot convert a value into a string, or the value to - convert is not a string while `stringizer` is None. - - See Also - -------- - literal_stringizer - - Notes - ----- - Graph attributes named 'directed', 'multigraph', 'node' or - 'edge', node attributes named 'id' or 'label', edge attributes - named 'source' or 'target' (or 'key' if `G` is a multigraph) - are ignored because these attribute names are used to encode the graph - structure. - - GML files are stored using a 7-bit ASCII encoding with any extended - ASCII characters (iso8859-1) appearing as HTML character entities. - Without specifying a `stringizer`/`destringizer`, the code is capable of - writing `int`/`float`/`str`/`dict`/`list` data as required by the GML - specification. For writing other data types, and for reading data other - than `str` you need to explicitly supply a `stringizer`/`destringizer`. - - For additional documentation on the GML file format, please see the - `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. - - See the module docstring :mod:`easygraph.readwrite.gml` for more details. - - Examples - -------- - >>> G = eg.Graph() - >>> G.add_node("1") - >>> print("\n".join(eg.generate_gml(G))) - graph [ - node [ - id 0 - label "1" - ] - ] - """ - valid_keys = re.compile("^[A-Za-z][0-9A-Za-z_]*$") - - def stringize(key, value, ignored_keys, indent, in_list=False): - if not isinstance(key, str): - raise EasyGraphError(f"{key!r} is not a string") - if not valid_keys.match(key): - raise EasyGraphError(f"{key!r} is not a valid key") - if not isinstance(key, str): - key = str(key) - if key not in ignored_keys: - if isinstance(value, (int, bool)): - if key == "label": - yield indent + key + ' "' + str(value) + '"' - elif value is True: - # python bool is an instance of int - yield indent + key + " 1" - elif value is False: - yield indent + key + " 0" - # GML only supports signed 32-bit integers - elif value < -(2**31) or value >= 2**31: - yield indent + key + ' "' + str(value) + '"' - else: - yield indent + key + " " + str(value) - elif isinstance(value, float): - text = repr(value).upper() - # GML matches INF to keys, so prepend + to INF. Use repr(float(*)) - # instead of string literal to future proof against changes to repr. - if text == repr(float("inf")).upper(): - text = "+" + text - else: - # GML requires that a real literal contain a decimal point, but - # repr may not output a decimal point when the mantissa is - # integral and hence needs fixing. - epos = text.rfind("E") - if epos != -1 and text.find(".", 0, epos) == -1: - text = text[:epos] + "." + text[epos:] - if key == "label": - yield indent + key + ' "' + text + '"' - else: - yield indent + key + " " + text - elif isinstance(value, dict): - yield indent + key + " [" - next_indent = indent + " " - for key, value in value.items(): - yield from stringize(key, value, (), next_indent) - yield indent + "]" - elif ( - isinstance(value, (list, tuple)) - and key != "label" - and value - and not in_list - ): - if len(value) == 1: - yield indent + key + " " + f'"{LIST_START_VALUE}"' - for val in value: - yield from stringize(key, val, (), indent, True) - else: - if stringizer: - try: - value = stringizer(value) - except ValueError as err: - raise EasyGraphError( - f"{value!r} cannot be converted into a string" - ) from err - if not isinstance(value, str): - raise EasyGraphError(f"{value!r} is not a string") - yield indent + key + ' "' + escape(value) + '"' - - yield "graph [" - - # Output graph attributes - multigraph = G.is_multigraph() - if G.is_directed(): - yield " directed 1" - if multigraph: - yield " multigraph 1" - ignored_keys = {"directed", "multigraph", "node", "edge"} - for attr, value in G.graph.items(): - yield from stringize(attr, value, ignored_keys, " ") - - # Output node data - node_id = dict(zip(G, range(len(G)))) - ignored_keys = {"id", "label"} - for node, attrs in G.nodes.items(): - yield " node [" - yield " id " + str(node_id[node]) - yield from stringize("label", node, (), " ") - for attr, value in attrs.items(): - yield from stringize(attr, value, ignored_keys, " ") - yield " ]" - - # Output edge data - ignored_keys = {"source", "target"} - kwargs = {"data": True} - if multigraph: - ignored_keys.add("key") - kwargs["keys"] = True - for e in G.edges: - yield " edge [" - yield " source " + str(node_id[e[0]]) - yield " target " + str(node_id[e[1]]) - if multigraph: - yield from stringize("key", e[2], (), " ") - for attr, value in e[-1].items(): - yield from stringize(attr, value, ignored_keys, " ") - yield " ]" - yield "]"
- - -
[docs]@open_file(0, mode="rb") -def read_gml(path, label="label", destringizer=None): - """Read graph in GML format from `path`. - - Parameters - ---------- - path : filename or filehandle - The filename or filehandle to read from. - - label : string, optional - If not None, the parsed nodes will be renamed according to node - attributes indicated by `label`. Default value: 'label'. - - destringizer : callable, optional - A `destringizer` that recovers values stored as strings in GML. If it - cannot convert a string to a value, a `ValueError` is raised. Default - value : None. - - Returns - ------- - G : EasyGraph graph - The parsed graph. - - Raises - ------ - EasyGraphError - If the input cannot be parsed. - - See Also - -------- - write_gml, parse_gml - literal_destringizer - - Notes - ----- - GML files are stored using a 7-bit ASCII encoding with any extended - ASCII characters (iso8859-1) appearing as HTML character entities. - Without specifying a `stringizer`/`destringizer`, the code is capable of - writing `int`/`float`/`str`/`dict`/`list` data as required by the GML - specification. For writing other data types, and for reading data other - than `str` you need to explicitly supply a `stringizer`/`destringizer`. - - For additional documentation on the GML file format, please see the - `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. - - See the module docstring :mod:`easygraph.readwrite.gml` for more details. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> eg.write_gml(G, "test.gml") - - GML values are interpreted as strings by default: - - >>> H = eg.read_gml("test.gml") - >>> H.nodes - NodeView(('0', '1', '2', '3')) - - When a `destringizer` is provided, GML values are converted to the provided type. - For example, integer nodes can be recovered as shown below: - - >>> J = eg.read_gml("test.gml", destringizer=int) - >>> J.nodes - NodeView((0, 1, 2, 3)) - - """ - - def filter_lines(lines): - for line in lines: - try: - line = line.decode("ascii") - except UnicodeDecodeError as err: - raise EasyGraphError("input is not ASCII-encoded") from err - if not isinstance(line, str): - lines = str(lines) - if line and line[-1] == "\n": - line = line[:-1] - yield line - - G = parse_gml_lines(filter_lines(path), label, destringizer) - return G
- - -
[docs]@open_file(1, mode="wb") -def write_gml(G, path, stringizer=None): - """Write a graph `G` in GML format to the file or file handle `path`. - - Parameters - ---------- - G : EasyGraph graph - The graph to be converted to GML. - - path : filename or filehandle - The filename or filehandle to write. Files whose names end with .gz or - .bz2 will be compressed. - - stringizer : callable, optional - A `stringizer` which converts non-int/non-float/non-dict values into - strings. If it cannot convert a value into a string, it should raise a - `ValueError` to indicate that. Default value: None. - - Raises - ------ - EasyGraphError - If `stringizer` cannot convert a value into a string, or the value to - convert is not a string while `stringizer` is None. - - See Also - -------- - read_gml, generate_gml - literal_stringizer - - Notes - ----- - Graph attributes named 'directed', 'multigraph', 'node' or - 'edge', node attributes named 'id' or 'label', edge attributes - named 'source' or 'target' (or 'key' if `G` is a multigraph) - are ignored because these attribute names are used to encode the graph - structure. - - GML files are stored using a 7-bit ASCII encoding with any extended - ASCII characters (iso8859-1) appearing as HTML character entities. - Without specifying a `stringizer`/`destringizer`, the code is capable of - writing `int`/`float`/`str`/`dict`/`list` data as required by the GML - specification. For writing other data types, and for reading data other - than `str` you need to explicitly supply a `stringizer`/`destringizer`. - - Note that while we allow non-standard GML to be read from a file, we make - sure to write GML format. In particular, underscores are not allowed in - attribute names. - For additional documentation on the GML file format, please see the - `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. - - See the module docstring :mod:`easygraph.readwrite.gml` for more details. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> eg.write_gml(G, "test.gml") - - Filenames ending in .gz or .bz2 will be compressed. - - >>> eg.write_gml(G, "test.gml.gz") - """ - for line in generate_gml(G, stringizer): - path.write((line + "\n").encode("ascii"))
- - -def literal_stringizer(value): - msg = "literal_stringizer is deprecated and will be removed in 3.0." - warnings.warn(msg, DeprecationWarning) - - def stringize(value): - if isinstance(value, (int, bool)) or value is None: - if value is True: # GML uses 1/0 for boolean values. - buf.write(str(1)) - elif value is False: - buf.write(str(0)) - else: - buf.write(str(value)) - elif isinstance(value, str): - text = repr(value) - if text[0] != "u": - try: - value.encode("latin1") - except UnicodeEncodeError: - text = "u" + text - buf.write(text) - elif isinstance(value, (float, complex, str, bytes)): - buf.write(repr(value)) - elif isinstance(value, list): - buf.write("[") - first = True - for item in value: - if not first: - buf.write(",") - else: - first = False - stringize(item) - buf.write("]") - elif isinstance(value, tuple): - if len(value) > 1: - buf.write("(") - first = True - for item in value: - if not first: - buf.write(",") - else: - first = False - stringize(item) - buf.write(")") - elif value: - buf.write("(") - stringize(value[0]) - buf.write(",)") - else: - buf.write("()") - elif isinstance(value, dict): - buf.write("{") - first = True - for key, value in value.items(): - if not first: - buf.write(",") - else: - first = False - stringize(key) - buf.write(":") - stringize(value) - buf.write("}") - elif isinstance(value, set): - buf.write("{") - first = True - for item in value: - if not first: - buf.write(",") - else: - first = False - stringize(item) - buf.write("}") - else: - msg = "{value!r} cannot be converted into a Python literal" - raise ValueError(msg) - - buf = StringIO() - stringize(value) - return buf.getvalue() -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/graphml.html b/docs/_modules/easygraph/readwrite/graphml.html deleted file mode 100644 index 9f0ccbb0..00000000 --- a/docs/_modules/easygraph/readwrite/graphml.html +++ /dev/null @@ -1,1169 +0,0 @@ - - - - - - easygraph.readwrite.graphml — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.readwrite.graphml

-"""
-*******
-GraphML
-*******
-Read and write graphs in GraphML format.
-
-.. warning::
-
-    This parser uses the standard xml library present in Python, which is
-    insecure - see :doc:`library/xml` for additional information.
-    Only parse GraphML files you trust.
-
-This implementation does not support mixed graphs (directed and unidirected
-edges together), hyperedges, nested graphs, or ports.
-
-"GraphML is a comprehensive and easy-to-use file format for graphs. It
-consists of a language core to describe the structural properties of a
-graph and a flexible extension mechanism to add application-specific
-data. Its main features include support of
-
-    * directed, undirected, and mixed graphs,
-    * hypergraphs,
-    * hierarchical graphs,
-    * graphical representations,
-    * references to external data,
-    * application-specific attribute data, and
-    * light-weight parsers.
-
-Unlike many other file formats for graphs, GraphML does not use a
-custom syntax. Instead, it is based on XML and hence ideally suited as
-a common denominator for all kinds of services generating, archiving,
-or processing graphs."
-
-http://graphml.graphdrawing.org/
-
-Format
-------
-GraphML is an XML format.  See
-http://graphml.graphdrawing.org/specification.html for the specification and
-http://graphml.graphdrawing.org/primer/graphml-primer.html
-for examples.
-"""
-
-import warnings
-
-from collections import defaultdict
-
-import easygraph as eg
-
-from easygraph.utils import open_file
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = [
-    "write_graphml",
-    "read_graphml",
-    "generate_graphml",
-    "write_graphml_xml",
-    "write_graphml_lxml",
-    "parse_graphml",
-    "GraphMLWriter",
-    "GraphMLReader",
-]
-
-
-
[docs]@open_file(1, mode="wb") -def write_graphml_xml( - G, - path, - encoding="utf-8", - prettyprint=True, - infer_numeric_types=False, - named_key_ids=False, - edge_id_from_attribute=None, -): - """Write G in GraphML XML format to path - - Parameters - ---------- - G : graph - A easygraph graph - path : file or string - File or filename to write. - Filenames ending in .gz or .bz2 will be compressed. - encoding : string (optional) - Encoding for text data. - prettyprint : bool (optional) - If True use line breaks and indenting in output XML. - infer_numeric_types : boolean - Determine if numeric types should be generalized. - For example, if edges have both int and float 'weight' attributes, - we infer in GraphML that both are floats. - named_key_ids : bool (optional) - If True use attr.name as value for key elements' id attribute. - edge_id_from_attribute : dict key (optional) - If provided, the graphml edge id is set by looking up the corresponding - edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data, - the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> eg.write_graphml(G, "test.graphml") - - Notes - ----- - This implementation does not support mixed graphs (directed - and unidirected edges together) hyperedges, nested graphs, or ports. - """ - writer = GraphMLWriter( - encoding=encoding, - prettyprint=prettyprint, - infer_numeric_types=infer_numeric_types, - named_key_ids=named_key_ids, - edge_id_from_attribute=edge_id_from_attribute, - ) - writer.add_graph_element(G) - writer.dump(path)
- - -
[docs]@open_file(1, mode="wb") -def write_graphml_lxml( - G, - path, - encoding="utf-8", - prettyprint=True, - infer_numeric_types=False, - named_key_ids=False, - edge_id_from_attribute=None, -): - """Write G in GraphML XML format to path - - This function uses the LXML framework and should be faster than - the version using the xml library. - - Parameters - ---------- - G : graph - A easygraph graph - path : file or string - File or filename to write. - Filenames ending in .gz or .bz2 will be compressed. - encoding : string (optional) - Encoding for text data. - prettyprint : bool (optional) - If True use line breaks and indenting in output XML. - infer_numeric_types : boolean - Determine if numeric types should be generalized. - For example, if edges have both int and float 'weight' attributes, - we infer in GraphML that both are floats. - named_key_ids : bool (optional) - If True use attr.name as value for key elements' id attribute. - edge_id_from_attribute : dict key (optional) - If provided, the graphml edge id is set by looking up the corresponding - edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data, - the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> eg.write_graphml_lxml(G, "fourpath.graphml") - - Notes - ----- - This implementation does not support mixed graphs (directed - and unidirected edges together) hyperedges, nested graphs, or ports. - """ - try: - import lxml.etree as lxmletree - except ImportError: - return write_graphml_xml( - G, - path, - encoding, - prettyprint, - infer_numeric_types, - named_key_ids, - edge_id_from_attribute, - ) - - writer = GraphMLWriterLxml( - path, - graph=G, - encoding=encoding, - prettyprint=prettyprint, - infer_numeric_types=infer_numeric_types, - named_key_ids=named_key_ids, - edge_id_from_attribute=edge_id_from_attribute, - ) - writer.dump()
- - -
[docs]def generate_graphml( - G, - encoding="utf-8", - prettyprint=True, - named_key_ids=False, - edge_id_from_attribute=None, -): - """Generate GraphML lines for G - - Parameters - ---------- - G : graph - A easygraph graph - encoding : string (optional) - Encoding for text data. - prettyprint : bool (optional) - If True use line breaks and indenting in output XML. - named_key_ids : bool (optional) - If True use attr.name as value for key elements' id attribute. - edge_id_from_attribute : dict key (optional) - If provided, the graphml edge id is set by looking up the corresponding - edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data, - the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> linefeed = chr(10) # linefeed = \n - >>> s = linefeed.join(eg.generate_graphml(G)) - >>> for line in eg.generate_graphml(G): # doctest: +SKIP - ... print(line) - - Notes - ----- - This implementation does not support mixed graphs (directed and unidirected - edges together) hyperedges, nested graphs, or ports. - """ - writer = GraphMLWriter( - encoding=encoding, - prettyprint=prettyprint, - named_key_ids=named_key_ids, - edge_id_from_attribute=edge_id_from_attribute, - ) - writer.add_graph_element(G) - yield from str(writer).splitlines()
- - -
[docs]@open_file(0, mode="rb") -def read_graphml(path, node_type=str, edge_key_type=int, force_multigraph=False): - """Read graph in GraphML format from path. - - Parameters - ---------- - path : file or string - File or filename to write. - Filenames ending in .gz or .bz2 will be compressed. - - node_type: Python type (default: str) - Convert node ids to this type - - edge_key_type: Python type (default: int) - Convert graphml edge ids to this type. Multigraphs use id as edge key. - Non-multigraphs add to edge attribute dict with name "id". - - force_multigraph : bool (default: False) - If True, return a multigraph with edge keys. If False (the default) - return a multigraph when multiedges are in the graph. - - Returns - ------- - graph: EasyGraph graph - If parallel edges are present or `force_multigraph=True` then - a MultiGraph or MultiDiGraph is returned. Otherwise a Graph/DiGraph. - The returned graph is directed if the file indicates it should be. - - Notes - ----- - Default node and edge attributes are not propagated to each node and edge. - They can be obtained from `G.graph` and applied to node and edge attributes - if desired using something like this: - - >>> default_color = G.graph["node_default"]["color"] # doctest: +SKIP - >>> for node, data in G.nodes(data=True): # doctest: +SKIP - ... if "color" not in data: - ... data["color"] = default_color - >>> default_color = G.graph["edge_default"]["color"] # doctest: +SKIP - >>> for u, v, data in G.edges(data=True): # doctest: +SKIP - ... if "color" not in data: - ... data["color"] = default_color - - This implementation does not support mixed graphs (directed and unidirected - edges together), hypergraphs, nested graphs, or ports. - - For multigraphs the GraphML edge "id" will be used as the edge - key. If not specified then they "key" attribute will be used. If - there is no "key" attribute a default EasyGraph multigraph edge key - will be provided. - - Files with the yEd "yfiles" extension can be read. The type of the node's - shape is preserved in the `shape_type` node attribute. - - yEd compressed files ("file.graphmlz" extension) can be read by renaming - the file to "file.graphml.gz". - - """ - reader = GraphMLReader(node_type, edge_key_type, force_multigraph) - # need to check for multiple graphs - glist = list(reader(path=path)) - if len(glist) == 0: - # If no graph comes back, try looking for an incomplete header - header = b'<graphml xmlns="http://graphml.graphdrawing.org/xmlns">' - path.seek(0) - old_bytes = path.read() - new_bytes = old_bytes.replace(b"<graphml>", header) - glist = list(reader(string=new_bytes)) - if len(glist) == 0: - raise EasyGraphError("file not successfully read as graphml") - return glist[0]
- - -
[docs]def parse_graphml( - graphml_string, node_type=str, edge_key_type=int, force_multigraph=False -): - """Read graph in GraphML format from string. - - Parameters - ---------- - graphml_string : string - String containing graphml information - (e.g., contents of a graphml file). - - node_type: Python type (default: str) - Convert node ids to this type - - edge_key_type: Python type (default: int) - Convert graphml edge ids to this type. Multigraphs use id as edge key. - Non-multigraphs add to edge attribute dict with name "id". - - force_multigraph : bool (default: False) - If True, return a multigraph with edge keys. If False (the default) - return a multigraph when multiedges are in the graph. - - - Returns - ------- - graph: EasyGraph graph - If no parallel edges are found a Graph or DiGraph is returned. - Otherwise a MultiGraph or MultiDiGraph is returned. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> linefeed = chr(10) # linefeed = \n - >>> s = linefeed.join(eg.generate_graphml(G)) - >>> H = eg.parse_graphml(s) - - Notes - ----- - Default node and edge attributes are not propagated to each node and edge. - They can be obtained from `G.graph` and applied to node and edge attributes - if desired using something like this: - - >>> default_color = G.graph["node_default"]["color"] # doctest: +SKIP - >>> for node, data in G.nodes(data=True): # doctest: +SKIP - ... if "color" not in data: - ... data["color"] = default_color - >>> default_color = G.graph["edge_default"]["color"] # doctest: +SKIP - >>> for u, v, data in G.edges(data=True): # doctest: +SKIP - ... if "color" not in data: - ... data["color"] = default_color - - This implementation does not support mixed graphs (directed and unidirected - edges together), hypergraphs, nested graphs, or ports. - - For multigraphs the GraphML edge "id" will be used as the edge - key. If not specified then they "key" attribute will be used. If - there is no "key" attribute a default EasyGraph multigraph edge key - will be provided. - - """ - reader = GraphMLReader(node_type, edge_key_type, force_multigraph) - # need to check for multiple graphs - glist = list(reader(string=graphml_string)) - if len(glist) == 0: - # If no graph comes back, try looking for an incomplete header - header = '<graphml xmlns="http://graphml.graphdrawing.org/xmlns">' - new_string = graphml_string.replace("<graphml>", header) - glist = list(reader(string=new_string)) - if len(glist) == 0: - raise eg.EasyGraphError("file not successfully read as graphml") - return glist[0]
- - -class GraphML: - NS_GRAPHML = "http://graphml.graphdrawing.org/xmlns" - NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" - # xmlns:y="http://www.yworks.com/xml/graphml" - NS_Y = "http://www.yworks.com/xml/graphml" - SCHEMALOCATION = " ".join( - [ - "http://graphml.graphdrawing.org/xmlns", - "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd", - ] - ) - - def construct_types(self): - types = [ - (int, "integer"), # for Gephi GraphML bug - (str, "yfiles"), - (str, "string"), - (int, "int"), - (int, "long"), - (float, "float"), - (float, "double"), - (bool, "boolean"), - ] - - # These additions to types allow writing numpy types - try: - import numpy as np - except: - pass - else: - # prepend so that python types are created upon read (last entry wins) - types = [ - (np.float64, "float"), - (np.float32, "float"), - (np.float16, "float"), - (np.float_, "float"), - (np.int_, "int"), - (np.int8, "int"), - (np.int16, "int"), - (np.int32, "int"), - (np.int64, "int"), - (np.uint8, "int"), - (np.uint16, "int"), - (np.uint32, "int"), - (np.uint64, "int"), - (np.int_, "int"), - (np.intc, "int"), - (np.intp, "int"), - ] + types - - self.xml_type = dict(types) - self.python_type = dict(reversed(a) for a in types) - - # This page says that data types in GraphML follow Java(TM). - # http://graphml.graphdrawing.org/primer/graphml-primer.html#AttributesDefinition - # true and false are the only boolean literals: - # http://en.wikibooks.org/wiki/Java_Programming/Literals#Boolean_Literals - convert_bool = { - # We use data.lower() in actual use. - "true": True, - "false": False, - # Include integer strings for convenience. - "0": False, - 0: False, - "1": True, - 1: True, - } - - def get_xml_type(self, key): - """Wrapper around the xml_type dict that raises a more informative - exception message when a user attempts to use data of a type not - supported by GraphML.""" - try: - return self.xml_type[key] - except KeyError as err: - raise TypeError( - f"GraphML does not support type {type(key)} as data values." - ) from err - - -
[docs]class GraphMLWriter(GraphML): - def __init__( - self, - graph=None, - encoding="utf-8", - prettyprint=True, - infer_numeric_types=False, - named_key_ids=False, - edge_id_from_attribute=None, - ): - self.construct_types() - from xml.etree.ElementTree import Element - - self.myElement = Element - - self.infer_numeric_types = infer_numeric_types - self.prettyprint = prettyprint - self.named_key_ids = named_key_ids - self.edge_id_from_attribute = edge_id_from_attribute - self.encoding = encoding - self.xml = self.myElement( - "graphml", - { - "xmlns": self.NS_GRAPHML, - "xmlns:xsi": self.NS_XSI, - "xsi:schemaLocation": self.SCHEMALOCATION, - }, - ) - self.keys = {} - self.attributes = defaultdict(list) - self.attribute_types = defaultdict(set) - - if graph is not None: - self.add_graph_element(graph) - - def __str__(self): - from xml.etree.ElementTree import tostring - - if self.prettyprint: - self.indent(self.xml) - s = tostring(self.xml).decode(self.encoding) - return s - -
[docs] def attr_type(self, name, scope, value): - """Infer the attribute type of data named name. Currently this only - supports inference of numeric types. - - If self.infer_numeric_types is false, type is used. Otherwise, pick the - most general of types found across all values with name and scope. This - means edges with data named 'weight' are treated separately from nodes - with data named 'weight'. - """ - if self.infer_numeric_types: - types = self.attribute_types[(name, scope)] - - if len(types) > 1: - types = {self.get_xml_type(t) for t in types} - if "string" in types: - return str - elif "float" in types or "double" in types: - return float - else: - return int - else: - return list(types)[0] - else: - return type(value)
- -
[docs] def get_key(self, name, attr_type, scope, default): - keys_key = (name, attr_type, scope) - try: - return self.keys[keys_key] - except KeyError: - if self.named_key_ids: - new_id = name - else: - new_id = f"d{len(list(self.keys))}" - - self.keys[keys_key] = new_id - key_kwargs = { - "id": new_id, - "for": scope, - "attr.name": name, - "attr.type": attr_type, - } - key_element = self.myElement("key", **key_kwargs) - # add subelement for data default value if present - if default is not None: - default_element = self.myElement("default") - default_element.text = str(default) - key_element.append(default_element) - self.xml.insert(0, key_element) - return new_id
- -
[docs] def add_data(self, name, element_type, value, scope="all", default=None): - """ - Make a data element for an edge or a node. Keep a log of the - type in the keys table. - """ - if element_type not in self.xml_type: - raise eg.EasyGraphError( - f"GraphML writer does not support {element_type} as data values." - ) - keyid = self.get_key(name, self.get_xml_type(element_type), scope, default) - data_element = self.myElement("data", key=keyid) - data_element.text = str(value) - return data_element
- -
[docs] def add_attributes(self, scope, xml_obj, data, default): - """Appends attribute data to edges or nodes, and stores type information - to be added later. See add_graph_element. - """ - for k, v in data.items(): - self.attribute_types[(str(k), scope)].add(type(v)) - self.attributes[xml_obj].append([k, v, scope, default.get(k)])
- -
[docs] def add_nodes(self, G, graph_element): - default = G.graph.get("node_default", {}) - for node, data in G.nodes.items(): - node_element = self.myElement("node", id=str(node)) - self.add_attributes("node", node_element, data, default) - graph_element.append(node_element)
- -
[docs] def add_edges(self, G, graph_element): - if G.is_multigraph(): - for u, v, key, data in G.edges: - edge_element = self.myElement( - "edge", - source=str(u), - target=str(v), - id=str(data.get(self.edge_id_from_attribute)) - if self.edge_id_from_attribute - and self.edge_id_from_attribute in data - else str(key), - ) - default = G.graph.get("edge_default", {}) - self.add_attributes("edge", edge_element, data, default) - graph_element.append(edge_element) - else: - for u, v, data in G.edges: - if self.edge_id_from_attribute and self.edge_id_from_attribute in data: - # select attribute to be edge id - edge_element = self.myElement( - "edge", - source=str(u), - target=str(v), - id=str(data.get(self.edge_id_from_attribute)), - ) - else: - # default: no edge id - edge_element = self.myElement("edge", source=str(u), target=str(v)) - default = G.graph.get("edge_default", {}) - self.add_attributes("edge", edge_element, data, default) - graph_element.append(edge_element)
- -
[docs] def add_graph_element(self, G): - """ - Serialize graph G in GraphML to the stream. - """ - if G.is_directed(): - default_edge_type = "directed" - else: - default_edge_type = "undirected" - - graphid = G.graph.pop("id", None) - if graphid is None: - graph_element = self.myElement("graph", edgedefault=default_edge_type) - else: - graph_element = self.myElement( - "graph", edgedefault=default_edge_type, id=graphid - ) - default = {} - data = { - k: v - for (k, v) in G.graph.items() - if k not in ["node_default", "edge_default"] - } - self.add_attributes("graph", graph_element, data, default) - self.add_nodes(G, graph_element) - self.add_edges(G, graph_element) - - # self.attributes contains a mapping from XML Objects to a list of - # data that needs to be added to them. - # We postpone processing in order to do type inference/generalization. - # See self.attr_type - for xml_obj, data in self.attributes.items(): - for k, v, scope, default in data: - xml_obj.append( - self.add_data( - str(k), self.attr_type(k, scope, v), str(v), scope, default - ) - ) - self.xml.append(graph_element)
- -
[docs] def add_graphs(self, graph_list): - """Add many graphs to this GraphML document.""" - for G in graph_list: - self.add_graph_element(G)
- -
[docs] def dump(self, stream): - from xml.etree.ElementTree import ElementTree - - if self.prettyprint: - self.indent(self.xml) - document = ElementTree(self.xml) - document.write(stream, encoding=self.encoding, xml_declaration=True)
- -
[docs] def indent(self, elem, level=0): - # in-place prettyprint formatter - i = "\n" + level * " " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - self.indent(elem, level + 1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i
- - -class IncrementalElement: - """Wrapper for _IncrementalWriter providing an Element like interface. - - This wrapper does not intend to be a complete implementation but rather to - deal with those calls used in GraphMLWriter. - """ - - def __init__(self, xml, prettyprint): - self.xml = xml - self.prettyprint = prettyprint - - def append(self, element): - self.xml.write(element, pretty_print=self.prettyprint) - - -class GraphMLWriterLxml(GraphMLWriter): - def __init__( - self, - path, - graph=None, - encoding="utf-8", - prettyprint=True, - infer_numeric_types=False, - named_key_ids=False, - edge_id_from_attribute=None, - ): - self.construct_types() - import lxml.etree as lxmletree - - self.myElement = lxmletree.Element - - self._encoding = encoding - self._prettyprint = prettyprint - self.named_key_ids = named_key_ids - self.edge_id_from_attribute = edge_id_from_attribute - self.infer_numeric_types = infer_numeric_types - - self._xml_base = lxmletree.xmlfile(path, encoding=encoding) - self._xml = self._xml_base.__enter__() - self._xml.write_declaration() - - # We need to have a xml variable that support insertion. This call is - # used for adding the keys to the document. - # We will store those keys in a plain list, and then after the graph - # element is closed we will add them to the main graphml element. - self.xml = [] - self._keys = self.xml - self._graphml = self._xml.element( - "graphml", - { - "xmlns": self.NS_GRAPHML, - "xmlns:xsi": self.NS_XSI, - "xsi:schemaLocation": self.SCHEMALOCATION, - }, - ) - self._graphml.__enter__() - self.keys = {} - self.attribute_types = defaultdict(set) - - if graph is not None: - self.add_graph_element(graph) - - def add_graph_element(self, G): - """ - Serialize graph G in GraphML to the stream. - """ - if G.is_directed(): - default_edge_type = "directed" - else: - default_edge_type = "undirected" - - graphid = G.graph.pop("id", None) - if graphid is None: - graph_element = self._xml.element("graph", edgedefault=default_edge_type) - else: - graph_element = self._xml.element( - "graph", edgedefault=default_edge_type, id=graphid - ) - - # gather attributes types for the whole graph - # to find the most general numeric format needed. - # Then pass through attributes to create key_id for each. - graphdata = { - k: v - for k, v in G.graph.items() - if k not in ("node_default", "edge_default") - } - node_default = G.graph.get("node_default", {}) - edge_default = G.graph.get("edge_default", {}) - # Graph attributes - for k, v in graphdata.items(): - self.attribute_types[(str(k), "graph")].add(type(v)) - for k, v in graphdata.items(): - element_type = self.get_xml_type(self.attr_type(k, "graph", v)) - self.get_key(str(k), element_type, "graph", None) - # Nodes and data - for node, d in G.nodes.items(): - for k, v in d.items(): - self.attribute_types[(str(k), "node")].add(type(v)) - for node, d in G.nodes.items(): - for k, v in d.items(): - T = self.get_xml_type(self.attr_type(k, "node", v)) - self.get_key(str(k), T, "node", node_default.get(k)) - # Edges and data - if G.is_multigraph(): - for u, v, ekey, d in G.edges: - for k, v in d.items(): - self.attribute_types[(str(k), "edge")].add(type(v)) - for u, v, ekey, d in G.edges: - for k, v in d.items(): - T = self.get_xml_type(self.attr_type(k, "edge", v)) - self.get_key(str(k), T, "edge", edge_default.get(k)) - else: - for u, v, d in G.edges: - for k, v in d.items(): - self.attribute_types[(str(k), "edge")].add(type(v)) - for u, v, d in G.edges: - for k, v in d.items(): - T = self.get_xml_type(self.attr_type(k, "edge", v)) - self.get_key(str(k), T, "edge", edge_default.get(k)) - - # Now add attribute keys to the xml file - for key in self.xml: - self._xml.write(key, pretty_print=self._prettyprint) - - # The incremental_writer writes each node/edge as it is created - incremental_writer = IncrementalElement(self._xml, self._prettyprint) - with graph_element: - self.add_attributes("graph", incremental_writer, graphdata, {}) - self.add_nodes(G, incremental_writer) # adds attributes too - self.add_edges(G, incremental_writer) # adds attributes too - - def add_attributes(self, scope, xml_obj, data, default): - """Appends attribute data.""" - for k, v in data.items(): - data_element = self.add_data( - str(k), self.attr_type(str(k), scope, v), str(v), scope, default.get(k) - ) - xml_obj.append(data_element) - - def __str__(self): - return object.__str__(self) - - def dump(self): - self._graphml.__exit__(None, None, None) - self._xml_base.__exit__(None, None, None) - - -# default is lxml is present. -write_graphml = write_graphml_lxml - - -
[docs]class GraphMLReader(GraphML): - """Read a GraphML document. Produces EasyGraph graph objects.""" - - def __init__(self, node_type=str, edge_key_type=int, force_multigraph=False): - self.construct_types() - self.node_type = node_type - self.edge_key_type = edge_key_type - self.multigraph = force_multigraph # If False, test for multiedges - self.edge_ids = {} # dict mapping (u,v) tuples to edge id attributes - - def __call__(self, path=None, string=None): - from xml.etree.ElementTree import ElementTree - from xml.etree.ElementTree import fromstring - - if path is not None: - self.xml = ElementTree(file=path) - elif string is not None: - self.xml = fromstring(string) - else: - raise ValueError("Must specify either 'path' or 'string' as kwarg") - (keys, defaults) = self.find_graphml_keys(self.xml) - for g in self.xml.findall(f"{{{self.NS_GRAPHML}}}graph"): - yield self.make_graph(g, keys, defaults) - -
[docs] def make_graph(self, graph_xml, graphml_keys, defaults, G=None): - # set default graph type - edgedefault = graph_xml.get("edgedefault", None) - if G is None: - if edgedefault == "directed": - G = eg.MultiDiGraph() - else: - G = eg.MultiGraph() - # set defaults for graph attributes - G.graph["node_default"] = {} - G.graph["edge_default"] = {} - for key_id, value in defaults.items(): - key_for = graphml_keys[key_id]["for"] - name = graphml_keys[key_id]["name"] - python_type = graphml_keys[key_id]["type"] - if key_for == "node": - G.graph["node_default"].update({name: python_type(value)}) - if key_for == "edge": - G.graph["edge_default"].update({name: python_type(value)}) - # hyperedges are not supported - hyperedge = graph_xml.find(f"{{{self.NS_GRAPHML}}}hyperedge") - if hyperedge is not None: - raise eg.EasyGraphError("GraphML reader doesn't support hyperedges") - # add nodes - for node_xml in graph_xml.findall(f"{{{self.NS_GRAPHML}}}node"): - self.add_node(G, node_xml, graphml_keys, defaults) - # add edges - for edge_xml in graph_xml.findall(f"{{{self.NS_GRAPHML}}}edge"): - self.add_edge(G, edge_xml, graphml_keys) - # add graph data - data = self.decode_data_elements(graphml_keys, graph_xml) - G.graph.update(data) - - # switch to Graph or DiGraph if no parallel edges were found - if self.multigraph: - return G - - G = eg.DiGraph(G) if G.is_directed() else eg.Graph(G) - # add explicit edge "id" from file as attribute in eg graph. - eg.set_edge_attributes(G, values=self.edge_ids, name="id") - return G
- -
[docs] def add_node(self, G, node_xml, graphml_keys, defaults): - """Add a node to the graph.""" - # warn on finding unsupported ports tag - ports = node_xml.find(f"{{{self.NS_GRAPHML}}}port") - if ports is not None: - warnings.warn("GraphML port tag not supported.") - # find the node by id and cast it to the appropriate type - node_id = self.node_type(node_xml.get("id")) - # get data/attributes for node - data = self.decode_data_elements(graphml_keys, node_xml) - G.add_node(node_id, **data) - # get child nodes - if node_xml.attrib.get("yfiles.foldertype") == "group": - graph_xml = node_xml.find(f"{{{self.NS_GRAPHML}}}graph") - self.make_graph(graph_xml, graphml_keys, defaults, G)
- -
[docs] def add_edge(self, G, edge_element, graphml_keys): - """Add an edge to the graph.""" - # warn on finding unsupported ports tag - ports = edge_element.find(f"{{{self.NS_GRAPHML}}}port") - if ports is not None: - warnings.warn("GraphML port tag not supported.") - - # raise error if we find mixed directed and undirected edges - directed = edge_element.get("directed") - if G.is_directed() and directed == "false": - msg = "directed=false edge found in directed graph." - raise eg.EasyGraphError(msg) - if (not G.is_directed()) and directed == "true": - msg = "directed=true edge found in undi rected graph." - raise eg.EasyGraphError(msg) - - source = self.node_type(edge_element.get("source")) - target = self.node_type(edge_element.get("target")) - data = self.decode_data_elements(graphml_keys, edge_element) - # GraphML stores edge ids as an attribute - # EasyGraph uses them as keys in multigraphs too if no key - # attribute is specified - edge_id = edge_element.get("id") - if edge_id: - # self.edge_ids is used by `make_graph` method for non-multigraphs - self.edge_ids[source, target] = edge_id - try: - edge_id = self.edge_key_type(edge_id) - except ValueError: # Could not convert. - pass - else: - edge_id = data.get("key") - - if G.has_edge(source, target): - # mark this as a multigraph - self.multigraph = True - - # Use add_edges_from to avoid error with add_edge when `'key' in data` - # Note there is only one edge here... - G.add_edges_from([(source, target, edge_id, data)])
- -
[docs] def decode_data_elements(self, graphml_keys, obj_xml): - """Use the key information to decode the data XML if present.""" - data = {} - for data_element in obj_xml.findall(f"{{{self.NS_GRAPHML}}}data"): - key = data_element.get("key") - try: - data_name = graphml_keys[key]["name"] - data_type = graphml_keys[key]["type"] - except KeyError as err: - raise eg.EasyGraphError(f"Bad GraphML data: no key {key}") from err - text = data_element.text - # assume anything with subelements is a yfiles extension - if text is not None and len(list(data_element)) == 0: - if data_type == bool: - # Ignore cases. - # http://docs.oracle.com/javase/6/docs/api/java/lang/ - # Boolean.html#parseBoolean%28java.lang.String%29 - data[data_name] = self.convert_bool[text.lower()] - else: - data[data_name] = data_type(text) - elif len(list(data_element)) > 0: - # Assume yfiles as subelements, try to extract node_label - node_label = None - # set GenericNode's configuration as shape type - gn = data_element.find(f"{{{self.NS_Y}}}GenericNode") - if gn: - data["shape_type"] = gn.get("configuration") - for node_type in ["GenericNode", "ShapeNode", "SVGNode", "ImageNode"]: - pref = f"{{{self.NS_Y}}}{node_type}/{{{self.NS_Y}}}" - geometry = data_element.find(f"{pref}Geometry") - if geometry is not None: - data["x"] = geometry.get("x") - data["y"] = geometry.get("y") - if node_label is None: - node_label = data_element.find(f"{pref}NodeLabel") - shape = data_element.find(f"{pref}Shape") - if shape is not None: - data["shape_type"] = shape.get("type") - if node_label is not None: - data["label"] = node_label.text - - # check all the different types of edges avaivable in yEd. - for edge_type in [ - "PolyLineEdge", - "SplineEdge", - "QuadCurveEdge", - "BezierEdge", - "ArcEdge", - ]: - pref = f"{{{self.NS_Y}}}{edge_type}/{{{self.NS_Y}}}" - edge_label = data_element.find(f"{pref}EdgeLabel") - if edge_label is not None: - break - - if edge_label is not None: - data["label"] = edge_label.text - return data
- -
[docs] def find_graphml_keys(self, graph_element): - """Extracts all the keys and key defaults from the xml.""" - graphml_keys = {} - graphml_key_defaults = {} - for k in graph_element.findall(f"{{{self.NS_GRAPHML}}}key"): - attr_id = k.get("id") - attr_type = k.get("attr.type") - attr_name = k.get("attr.name") - yfiles_type = k.get("yfiles.type") - if yfiles_type is not None: - attr_name = yfiles_type - attr_type = "yfiles" - if attr_type is None: - attr_type = "string" - warnings.warn(f"No key type for id {attr_id}. Using string") - if attr_name is None: - raise eg.EasyGraphError(f"Unknown key for id {attr_id}.") - graphml_keys[attr_id] = { - "name": attr_name, - "type": self.python_type[attr_type], - "for": k.get("for"), - } - # check for "default" sub-element of key element - default = k.find(f"{{{self.NS_GRAPHML}}}default") - if default is not None: - # Handle default values identically to data element values - python_type = graphml_keys[attr_id]["type"] - if python_type == bool: - graphml_key_defaults[attr_id] = self.convert_bool[ - default.text.lower() - ] - else: - graphml_key_defaults[attr_id] = python_type(default.text) - return graphml_keys, graphml_key_defaults
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/graphviz.html b/docs/_modules/easygraph/readwrite/graphviz.html deleted file mode 100644 index e36284d3..00000000 --- a/docs/_modules/easygraph/readwrite/graphviz.html +++ /dev/null @@ -1,293 +0,0 @@ - - - - - - easygraph.readwrite.graphviz — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.readwrite.graphviz

-import easygraph as eg
-
-
-__all__ = ["write_dot", "read_dot", "from_agraph", "to_agraph"]
-
-
-
[docs]def from_agraph(A, create_using=None): - """Returns a EasyGraph Graph or DiGraph from a PyGraphviz graph. - - Parameters - ---------- - A : PyGraphviz AGraph - A graph created with PyGraphviz - - create_using : EasyGraph graph constructor, optional (default=None) - Graph type to create. If graph instance, then cleared before populated. - If `None`, then the appropriate Graph type is inferred from `A`. - - Examples - -------- - >>> K5 = eg.complete_graph(5) - >>> A = eg.to_agraph(K5) - >>> G = eg.from_agraph(A) - - Notes - ----- - The Graph G will have a dictionary G.graph_attr containing - the default graphviz attributes for graphs, nodes and edges. - - Default node attributes will be in the dictionary G.node_attr - which is keyed by node. - - Edge attributes will be returned as edge data in G. With - edge_attr=False the edge data will be the Graphviz edge weight - attribute or the value 1 if no edge weight attribute is found. - - """ - if create_using is None: - if A.is_directed(): - if A.is_strict(): - create_using = eg.DiGraph - else: - create_using = eg.MultiDiGraph - else: - if A.is_strict(): - create_using = eg.Graph - else: - create_using = eg.MultiGraph - - # assign defaults - N = eg.empty_graph(0, create_using) - if A.name is not None: - N.name = A.name - - # add graph attributes - N.graph.update(A.graph_attr) - - # add nodes, attributes to N.node_attr - for n in A.nodes(): - str_attr = {str(k): v for k, v in n.attr.items()} - N.add_node(str(n), **str_attr) - - # add edges, assign edge data as dictionary of attributes - for e in A.edges(): - u, v = str(e[0]), str(e[1]) - attr = dict(e.attr) - str_attr = {str(k): v for k, v in attr.items()} - if not N.is_multigraph(): - if e.name is not None: - str_attr["key"] = e.name - N.add_edge(u, v, **str_attr) - else: - N.add_edge(u, v, key=e.name, **str_attr) - - # add default attributes for graph, nodes, and edges - # hang them on N.graph_attr - N.graph["graph"] = dict(A.graph_attr) - N.graph["node"] = dict(A.node_attr) - N.graph["edge"] = dict(A.edge_attr) - return N
- - -
[docs]def to_agraph(N): - """Returns a pygraphviz graph from a EasyGraph graph N. - - Parameters - ---------- - N : EasyGraph graph - A graph created with EasyGraph - - Examples - -------- - >>> K5 = eg.complete_graph(5) - >>> A = eg.to_agraph(K5) - - Notes - ----- - If N has an dict N.graph_attr an attempt will be made first - to copy properties attached to the graph (see from_agraph) - and then updated with the calling arguments if any. - - """ - try: - import pygraphviz - except ImportError as err: - raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err - directed = N.is_directed() - strict = eg.number_of_selfloops(N) == 0 and not N.is_multigraph() - A = pygraphviz.AGraph(name=N.name, strict=strict, directed=directed) - - # default graph attributes - A.graph_attr.update(N.graph.get("graph", {})) - A.node_attr.update(N.graph.get("node", {})) - A.edge_attr.update(N.graph.get("edge", {})) - - A.graph_attr.update( - (k, v) for k, v in N.graph.items() if k not in ("graph", "node", "edge") - ) - - # add nodes - for n, nodedata in N.nodes.items(): - A.add_node(n) - # Add node data - a = A.get_node(n) - a.attr.update({k: str(v) for k, v in nodedata.items()}) - - # loop over edges - if N.is_multigraph(): - for u, v, key, edgedata in N.edges: - str_edgedata = {k: str(v) for k, v in edgedata.items() if k != "key"} - A.add_edge(u, v, key=str(key)) - # Add edge data - a = A.get_edge(u, v) - a.attr.update(str_edgedata) - - else: - for u, v, edgedata in N.edges: - str_edgedata = {k: str(v) for k, v in edgedata.items()} - A.add_edge(u, v) - # Add edge data - a = A.get_edge(u, v) - a.attr.update(str_edgedata) - - return A
- - -
[docs]def write_dot(G, path): - """Write EasyGraph graph G to Graphviz dot format on path. - - Parameters - ---------- - G : graph - A easygraph graph - path : filename - Filename or file handle to write - """ - A = to_agraph(G) - A.write(path) - A.clear() - return
- - -
[docs]def read_dot(path): - """Returns a EasyGraph graph from a dot file on path. - - Parameters - ---------- - path : file or string - File name or file handle to read. - """ - try: - import pygraphviz - except ImportError as err: - raise ImportError( - "read_dot() requires pygraphviz http://pygraphviz.github.io/" - ) from err - A = pygraphviz.AGraph(file=path) - gr = from_agraph(A) - A.clear() - return gr
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/json_graph/node_link.html b/docs/_modules/easygraph/readwrite/json_graph/node_link.html deleted file mode 100644 index d7290c64..00000000 --- a/docs/_modules/easygraph/readwrite/json_graph/node_link.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - easygraph.readwrite.json_graph.node_link — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.readwrite.json_graph.node_link

-from itertools import chain
-from itertools import count
-
-import easygraph as eg
-
-
-__all__ = ["node_link_graph"]
-
-
-_attrs = dict(source="source", target="target", name="id", key="key", link="links")
-
-
-def _to_tuple(x):
-    """Converts lists to tuples, including nested lists.
-
-    All other non-list inputs are passed through unmodified. This function is
-    intended to be used to convert potentially nested lists from json files
-    into valid nodes.
-
-    Examples
-    --------
-    >>> _to_tuple([1, 2, [3, 4]])
-    (1, 2, (3, 4))
-    """
-    if not isinstance(x, (tuple, list)):
-        return x
-    return tuple(map(_to_tuple, x))
-
-
-
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/pajek.html b/docs/_modules/easygraph/readwrite/pajek.html deleted file mode 100644 index c0d56774..00000000 --- a/docs/_modules/easygraph/readwrite/pajek.html +++ /dev/null @@ -1,450 +0,0 @@ - - - - - - easygraph.readwrite.pajek — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.readwrite.pajek

-# This file is part of the NetworkX distribution.
-
-# NetworkX is distributed with the 3-clause BSD license.
-
-
-# ::
-#    Copyright (C) 2004-2022, NetworkX Developers
-#    Aric Hagberg <hagberg@lanl.gov>
-#    Dan Schult <dschult@colgate.edu>
-#    Pieter Swart <swart@lanl.gov>
-#    All rights reserved.
-
-#    Redistribution and use in source and binary forms, with or without
-#    modification, are permitted provided that the following conditions are
-#    met:
-
-#      * Redistributions of source code must retain the above copyright
-#        notice, this list of conditions and the following disclaimer.
-
-#      * Redistributions in binary form must reproduce the above
-#        copyright notice, this list of conditions and the following
-#        disclaimer in the documentation and/or other materials provided
-#        with the distribution.
-
-#      * Neither the name of the NetworkX Developers nor the names of its
-#        contributors may be used to endorse or promote products derived
-#        from this software without specific prior written permission.
-
-#    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-#    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-#    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-#    A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-#    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-#    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-#    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-#    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-#    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-#    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-#    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-"""
-*****
-Pajek
-*****
-Read graphs in Pajek format.
-
-This implementation handles directed and undirected graphs including
-those with self loops and parallel edges.
-
-Format
-------
-See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm
-for format information.
-
-"""
-
-import warnings
-
-import easygraph as eg
-
-# import networkx as nx
-from easygraph.utils import open_file
-
-
-__all__ = ["read_pajek", "parse_pajek", "generate_pajek", "write_pajek"]
-
-
-
[docs]def generate_pajek(G): - """Generate lines in Pajek graph format. - - Parameters - ---------- - G : graph - A EasyGraph graph - - References - ---------- - See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm - for format information. - """ - if G.name == "": - name = "EasyGraph" - else: - name = G.name - # Apparently many Pajek format readers can't process this line - # So we'll leave it out for now. - # yield '*network %s'%name - - # write nodes with attributes - yield f"*vertices {G.order()}" - nodes = list(G) - # make dictionary mapping nodes to integers - nodenumber = dict(zip(nodes, range(1, len(nodes) + 1))) - for n in nodes: - # copy node attributes and pop mandatory attributes - # to avoid duplication. - na = G.nodes.get(n, {}).copy() - x = na.pop("x", 0.0) - y = na.pop("y", 0.0) - try: - id = int(na.pop("id", nodenumber[n])) - except ValueError as err: - err.args += ( - "Pajek format requires 'id' to be an int()." - " Refer to the 'Relabeling nodes' section.", - ) - raise - nodenumber[n] = id - shape = na.pop("shape", "ellipse") - s = " ".join(map(make_qstr, (id, n, x, y, shape))) - # only optional attributes are left in na. - for k, v in na.items(): - if isinstance(v, str) and v.strip() != "": - s += f" {make_qstr(k)} {make_qstr(v)}" - else: - warnings.warn( - f"Node attribute {k} is not processed." - f" {('Empty attribute' if isinstance(v, str) else 'Non-string attribute')}." - ) - yield s - - # write edges with attributes - if G.is_directed(): - yield "*arcs" - else: - yield "*edges" - # from icecream import ic - # ic(G.edges) - # if isinstance(G, MultiGraph) - for u, v, *edgedata in G.edges: - # if len(edgedata) > 1: - # edgedata = edgedata[1] - # else: - # edgedata = edgedata[0] - edgedata = edgedata[-1] - d = edgedata.copy() - value = d.pop("weight", 1.0) # use 1 as default edge value - s = " ".join(map(make_qstr, (nodenumber[u], nodenumber[v], value))) - for k, v in d.items(): - if isinstance(v, str) and v.strip() != "": - s += f" {make_qstr(k)} {make_qstr(v)}" - else: - warnings.warn( - f"Edge attribute {k} is not processed." - f" {('Empty attribute' if isinstance(v, str) else 'Non-string attribute')}." - ) - yield s
- - -
[docs]@open_file(1, mode="wb") -def write_pajek(G, path, encoding="UTF-8"): - """Write graph in Pajek format to path. - - Parameters - ---------- - G : graph - A EasyGraph graph - path : file or string - File or filename to write. - Filenames ending in .gz or .bz2 will be compressed. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> eg.write_pajek(G, "test.net") - - Warnings - -------- - Optional node attributes and edge attributes must be non-empty strings. - Otherwise it will not be written into the file. You will need to - convert those attributes to strings if you want to keep them. - - References - ---------- - See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm - for format information. - """ - for line in generate_pajek(G): - line += "\n" - path.write(line.encode(encoding))
- - -
[docs]@open_file(0, mode="rb") -def read_pajek(path): - """Read graph in Pajek format from path. - - Parameters - ---------- - path : file or string - File or filename to write. - Filenames ending in .gz or .bz2 will be uncompressed. - - Returns - ------- - G : EasyGraph MultiGraph or MultiDiGraph. - - Examples - -------- - >>> G = eg.path_graph(4) - >>> eg.write_pajek(G, "test.net") - >>> G = eg.read_pajek("test.net") - - To create a Graph instead of a MultiGraph use - - >>> G1 = eg.Graph(G) - - References - ---------- - See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm - for format information. - """ - lines = (line.decode() for line in path) - # with open(path) as f: - # lines = f.readlines() - return parse_pajek(lines)
- - -
[docs]def parse_pajek(lines): - """Parse Pajek format graph from string or iterable. - - Parameters - ---------- - lines : string or iterable - Data in Pajek format. - - Returns - ------- - G : EasyGraph graph - - See Also - -------- - read_pajek - - """ - import shlex - - # multigraph=False - if isinstance(lines, str): - lines = iter(lines.split("\n")) - # from itertools import tee - # lines, lines2 = tee(lines) - # from icecream import ic - # ic(next(lines2)) - lines = iter([line.rstrip("\n") for line in lines]) - G = eg.MultiDiGraph() # are multiedges allowed in Pajek? assume yes - labels = [] # in the order of the file, needed for matrix - while lines: - try: - l = next(lines) - except: # EOF - break - if l.lower().startswith("*network"): - try: - label, name = l.split(None, 1) - except ValueError: - # Line was not of the form: *network NAME - pass - else: - G.graph["name"] = name - elif l.lower().startswith("*vertices"): - nodelabels = {} - l, nnodes = l.split() - for i in range(int(nnodes)): - l = next(lines) - try: - splitline = [x for x in shlex.split(str(l))] - except AttributeError: - splitline = shlex.split(str(l)) - id, label = splitline[0:2] - labels.append(label) - G.add_node(label) - nodelabels[id] = label - G.nodes[label]["id"] = id - try: - x, y, shape = splitline[2:5] - G.nodes[label].update( - {"x": float(x), "y": float(y), "shape": shape} - ) - except: - pass - extra_attr = zip(splitline[5::2], splitline[6::2]) - G.nodes[label].update(extra_attr) - elif l.lower().startswith("*edges") or l.lower().startswith("*arcs"): - if l.lower().startswith("*edge"): - # switch from multidigraph to multigraph - G = eg.MultiGraph(G) - if l.lower().startswith("*arcs"): - # switch to directed with multiple arcs for each existing edge - # G = G.to_directed() - pass - for l in lines: - try: - splitline = [x for x in shlex.split(str(l))] - except AttributeError: - splitline = shlex.split(str(l)) - - if len(splitline) < 2: - continue - ui, vi = splitline[0:2] - u = nodelabels.get(ui, ui) - v = nodelabels.get(vi, vi) - # parse the data attached to this edge and put in a dictionary - edge_data = {} - try: - # there should always be a single value on the edge? - w = splitline[2:3] - edge_data.update({"weight": float(w[0])}) - except: - pass - # if there isn't, just assign a 1 - # edge_data.update({'value':1}) - extra_attr = zip(splitline[3::2], splitline[4::2]) - edge_data.update(extra_attr) - # if G.has_edge(u,v): - # multigraph=True - G.add_edge(u, v, **edge_data) - elif l.lower().startswith("*matrix"): - G = eg.DiGraph(G) - adj_list = ( - (labels[row], labels[col], {"weight": int(data)}) - for (row, line) in enumerate(lines) - for (col, data) in enumerate(line.split()) - if int(data) != 0 - ) - G.add_edges_from(adj_list) - - return G
- - -def make_qstr(t): - """Returns the string representation of t. - Add outer double-quotes if the string has a space. - """ - if not isinstance(t, str): - t = str(t) - if " " in t: - t = f'"{t}"' - return t -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/pickle.html b/docs/_modules/easygraph/readwrite/pickle.html deleted file mode 100644 index 73706bf7..00000000 --- a/docs/_modules/easygraph/readwrite/pickle.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - easygraph.readwrite.pickle — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • »
  • -
  • Module code »
  • -
  • easygraph.readwrite.pickle
  • -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.readwrite.pickle

-#!/usr/bin/env python3
-
-
-
[docs]def read_pickle(file_name): - import pickle - - with open(file_name, "rb") as f: - return pickle.load(f)
- - -
[docs]def write_pickle(file_name, obj): - import pickle - - with open(file_name, "wb") as f: - pickle.dump(obj, f)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/readwrite/ucinet.html b/docs/_modules/easygraph/readwrite/ucinet.html deleted file mode 100644 index 6ae3950f..00000000 --- a/docs/_modules/easygraph/readwrite/ucinet.html +++ /dev/null @@ -1,440 +0,0 @@ - - - - - - easygraph.readwrite.ucinet — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.readwrite.ucinet

-"""
-**************
-UCINET DL
-**************
-Read and write graphs in UCINET DL format.
-This implementation currently supports only the 'fullmatrix' data format.
-Format
-------
-The UCINET DL format is the most common file format used by UCINET package.
-Basic example:
-DL N = 5
-Data:
-0 1 1 1 1
-1 0 1 0 0
-1 1 0 0 1
-1 0 0 0 0
-1 0 1 0 0
-References
-----------
-    See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm
-    for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm
-"""
-
-
-import re
-import shlex
-
-import easygraph as eg
-import numpy as np
-
-from easygraph.utils import open_file
-
-
-__all__ = ["generate_ucinet", "read_ucinet", "parse_ucinet", "write_ucinet"]
-
-
-
[docs]def generate_ucinet(G): - """Generate lines in UCINET graph format. - Parameters - ---------- - G : graph - A EasyGraph graph - Examples - -------- - Notes - ----- - The default format 'fullmatrix' is used (for UCINET DL format). - - References - ---------- - See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm - for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm - """ - - n = G.number_of_nodes() - nodes = sorted(list(G.nodes)) - yield "dl n=%i format=fullmatrix" % n - - # Labels - try: - int(nodes[0]) - except ValueError: - s = "labels:\n" - for label in nodes: - s += label + " " - yield s - - yield "data:" - - yield str(np.asmatrix(eg.to_numpy_array(G, nodelist=nodes, dtype=int))).replace( - "[", " " - ).replace("]", " ").lstrip().rstrip()
- - -
[docs]@open_file(0, mode="rb") -def read_ucinet(path, encoding="UTF-8"): - """Read graph in UCINET format from path. - Parameters - ---------- - path : file or string - File or filename to read. - Filenames ending in .gz or .bz2 will be uncompressed. - Returns - ------- - G : EasyGraph MultiGraph or MultiDiGraph. - Examples - -------- - >>> G=eg.path_graph(4) - >>> eg.write_ucinet(G, "test.dl") - >>> G=eg.read_ucinet("test.dl") - To create a Graph instead of a MultiGraph use - >>> G1=eg.Graph(G) - See Also - -------- - parse_ucinet() - References - ---------- - See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm - for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm - """ - lines = (line.decode(encoding) for line in path) - return parse_ucinet(lines)
- - -
[docs]@open_file(1, mode="wb") -def write_ucinet(G, path, encoding="UTF-8"): - """Write graph in UCINET format to path. - Parameters - ---------- - G : graph - A EasyGraph graph - path : file or string - File or filename to write. - Filenames ending in .gz or .bz2 will be compressed. - Examples - -------- - >>> G=eg.path_graph(4) - >>> eg.write_ucinet(G, "test.net") - References - ---------- - See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm - for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm - """ - for line in generate_ucinet(G): - line += "\n" - path.write(line.encode(encoding))
- - -
[docs]def parse_ucinet(lines): - """Parse UCINET format graph from string or iterable. - Currently only the 'fullmatrix', 'nodelist1' and 'nodelist1b' formats are supported. - Parameters - ---------- - lines : string or iterable - Data in UCINET format. - Returns - ------- - G : EasyGraph graph - See Also - -------- - read_ucinet() - References - ---------- - See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm - for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm - """ - from numpy import genfromtxt - from numpy import isnan - from numpy import reshape - - G = eg.MultiDiGraph() - - if not isinstance(lines, str): - s = "" - for line in lines: - if type(line) == bytes: - s += line.decode("utf-8") - else: - s += line - lines = s - lexer = shlex.shlex(lines.lower()) - lexer.whitespace += ",=" - lexer.whitespace_split = True - - number_of_nodes = 0 - number_of_matrices = 0 - nr = 0 # number of rows (rectangular matrix) - nc = 0 # number of columns (rectangular matrix) - ucinet_format = "fullmatrix" # Format by default - labels = {} # Contains labels of nodes - row_labels_embedded = False # Whether labels are embedded in data or not - cols_labels_embedded = False - diagonal = True # whether the main diagonal is present or absent - - KEYWORDS = ("format", "data:", "labels:") # TODO remove ':' in keywords - - while lexer: - try: - token = next(lexer) - except StopIteration: - break - # print "Token : %s" % token - if token.startswith("n"): - if token.startswith("nr"): - nr = int(get_param(r"\d+", token, lexer)) - number_of_nodes = max(nr, nc) - elif token.startswith("nc"): - nc = int(get_param(r"\d+", token, lexer)) - number_of_nodes = max(nr, nc) - elif token.startswith("nm"): - number_of_matrices = int(get_param(r"\d+", token, lexer)) - else: - number_of_nodes = int(get_param(r"\d+", token, lexer)) - nr = number_of_nodes - nc = number_of_nodes - - elif token.startswith("diagonal"): - diagonal = get_param("present|absent", token, lexer) - - elif token.startswith("format"): - ucinet_format = get_param( - """^(fullmatrix|upperhalf|lowerhalf|nodelist1|nodelist2|nodelist1b|\ -edgelist1|edgelist2|blockmatrix|partition)$""", - token, - lexer, - ) - - # TODO : row and columns labels - elif token.startswith("row"): # Row labels - pass - elif token.startswith("column"): # Columns labels - pass - - elif token.startswith("labels"): - token = next(lexer) - i = 0 - while token not in KEYWORDS: - if token.startswith("embedded"): - row_labels_embedded = True - cols_labels_embedded = True - break - else: - labels[i] = token.replace( - '"', "" - ) # for labels with embedded spaces - i += 1 - try: - token = next(lexer) - except StopIteration: - break - elif token.startswith("data"): - break - - data_lines = lines.lower().split("data:", 1)[1] - # Generate edges - params = {} - if cols_labels_embedded: - # params['names'] = True - labels = dict(zip(range(0, nc), data_lines.splitlines()[1].split())) - # params['skip_header'] = 2 # First character is \n - if row_labels_embedded: # Skip first column - # TODO rectangular case : labels can differ from rows to columns - # params['usecols'] = range(1, nc + 1) - pass - - if ucinet_format == "fullmatrix": - # In Python3 genfromtxt requires bytes string - try: - data_lines = bytes(data_lines, "utf-8") - except TypeError: - pass - # Do not use splitlines() because it is not necessarily written as a square matrix - data = genfromtxt([data_lines], case_sensitive=False, **params) - if cols_labels_embedded or row_labels_embedded: - # data = insert(data, 0, float('nan')) - data = data[~isnan(data)] - mat = reshape(data, (max(number_of_nodes, nr), -1)) - G = eg.from_numpy_array(mat, create_using=eg.MultiDiGraph()) - - elif ucinet_format in ( - "nodelist1", - "nodelist1b", - ): # Since genfromtxt only accepts square matrix... - s = "" - for i, line in enumerate(data_lines.splitlines()): - row = line.split() - if row: - if ucinet_format == "nodelist1b" and row[0] == "0": - pass - else: - for neighbor in row[1:]: - if ucinet_format == "nodelist1": - source = row[0] - else: - source = str(i) - s += source + " " + neighbor + "\n" - - G = eg.parse_edgelist( - s.splitlines(), - nodetype=str if row_labels_embedded and cols_labels_embedded else int, - create_using=eg.MultiDiGraph(), - ) - - if not row_labels_embedded or not cols_labels_embedded: - G = eg.relabel_nodes(G, dict(zip(list(G.nodes), [i - 1 for i in G.nodes]))) - - elif ucinet_format == "edgelist1": - G = eg.parse_edgelist( - data_lines.splitlines(), - nodetype=str if row_labels_embedded and cols_labels_embedded else int, - create_using=eg.MultiDiGraph(), - ) - - if not row_labels_embedded or not cols_labels_embedded: - G = eg.relabel_nodes(G, dict(zip(list(G.nodes), [i - 1 for i in G.nodes]))) - - # Relabel nodes - if labels: - try: - if len(list(G.nodes)) < number_of_nodes: - G.add_nodes_from( - labels.values() if labels else range(0, number_of_nodes) - ) - G = eg.relabel_nodes(G, labels) - except KeyError: - pass # Nodes already labelled - - return G
- - -def get_param(regex, token, lines): - """ - Get a parameter value in UCINET DL file - :param regex: string with the regex matching the parameter value - :param token: token (string) in which we search for the parameter - :param lines: to iterate through the next tokens - :return: - """ - n = token - query = re.search(regex, n) - while query is None: - try: - n = next(lines) - except StopIteration: - raise Exception("Parameter %s value not recognized" % token) - query = re.search(regex, n) - return query.group() -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/alias.html b/docs/_modules/easygraph/utils/alias.html deleted file mode 100644 index b83c8da1..00000000 --- a/docs/_modules/easygraph/utils/alias.html +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - easygraph.utils.alias — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.alias

-__all__ = ["create_alias_table", "alias_sample", "alias_setup", "alias_draw"]
-
-
-
[docs]def create_alias_table(area_ratio): - """ - Parameters - --------- - area_ratio : - sum(area_ratio)=1 - - Returns - ---------- - 1. accept - 2. alias - - """ - import numpy as np - - l = len(area_ratio) - accept, alias = [0] * l, [0] * l - small, large = [], [] - area_ratio_ = np.array(area_ratio) * l - for i, prob in enumerate(area_ratio_): - if prob < 1.0: - small.append(i) - else: - large.append(i) - - while small and large: - small_idx, large_idx = small.pop(), large.pop() - accept[small_idx] = area_ratio_[small_idx] - alias[small_idx] = large_idx - area_ratio_[large_idx] = area_ratio_[large_idx] - (1 - area_ratio_[small_idx]) - if area_ratio_[large_idx] < 1.0: - small.append(large_idx) - else: - large.append(large_idx) - - while large: - large_idx = large.pop() - accept[large_idx] = 1 - while small: - small_idx = small.pop() - accept[small_idx] = 1 - - return accept, alias
- - -
[docs]def alias_sample(accept, alias): - """ - Parameters - ---------- - accept : - - alias : - - Returns - ---------- - sample index - """ - import numpy as np - - N = len(accept) - i = int(np.random.random() * N) - r = np.random.random() - if r < accept[i]: - return i - else: - return alias[i]
- - -
[docs]def alias_draw(J, q): - import numpy as np - - """ - Draw sample from a non-uniform discrete distribution using alias sampling. - """ - K = len(J) - - kk = int(np.floor(np.random.rand() * K)) - if np.random.rand() < q[kk]: - return kk - else: - return J[kk]
- - -
[docs]def alias_setup(probs): - import numpy as np - - """ - Compute utility lists for non-uniform sampling from discrete distributions. - Refer to https://hips.seas.harvard.edu/blog/2013/03/03/the-alias-method-efficient-sampling-with-many-discrete-outcomes/ - for details - """ - K = len(probs) - q = np.zeros(K) - J = np.zeros(K, dtype=int) - - smaller = [] - larger = [] - for kk, prob in enumerate(probs): - q[kk] = K * prob - if q[kk] < 1.0: - smaller.append(kk) - else: - larger.append(kk) - - while len(smaller) > 0 and len(larger) > 0: - small = smaller.pop() - large = larger.pop() - - J[small] = large - q[large] = q[large] + q[small] - 1.0 - if q[large] < 1.0: - smaller.append(large) - else: - larger.append(large) - - return J, q
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/convert_class.html b/docs/_modules/easygraph/utils/convert_class.html deleted file mode 100644 index d610e258..00000000 --- a/docs/_modules/easygraph/utils/convert_class.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - easygraph.utils.convert_class — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • »
  • -
  • Module code »
  • -
  • easygraph.utils.convert_class
  • -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.utils.convert_class

-__all__ = [
-    "convert_graph_class",
-]
-
-
-
[docs]def convert_graph_class(G, graph_class): - _G = graph_class() - _G.graph.update(G.graph) - for node, node_attrs in G.nodes.items(): - dict_attrs = {} - for key, value in node_attrs: - dict_attrs[key] = value - _G.add_node(node, **dict_attrs) - for u, v, edge_attrs in G.edges: - dict_attrs = {} - for key, value in edge_attrs.items(): - dict_attrs[key] = value - _G.add_edge(u, v, **dict_attrs) - return _G
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/convert_to_matrix.html b/docs/_modules/easygraph/utils/convert_to_matrix.html deleted file mode 100644 index b459484a..00000000 --- a/docs/_modules/easygraph/utils/convert_to_matrix.html +++ /dev/null @@ -1,1115 +0,0 @@ - - - - - - easygraph.utils.convert_to_matrix — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.utils.convert_to_matrix

-import itertools
-
-import easygraph as eg
-
-
-__all__ = [
-    "to_numpy_matrix",
-    "from_numpy_array",
-    "to_numpy_array",
-    "from_pandas_adjacency",
-    "from_pandas_edgelist",
-    "from_scipy_sparse_matrix",
-    "to_scipy_sparse_matrix",
-    "to_scipy_sparse_array",
-]
-
-
-
[docs]def to_scipy_sparse_array(G, nodelist=None, dtype=None, weight="weight", format="csr"): - """Returns the graph adjacency matrix as a SciPy sparse array. - - Parameters - ---------- - G : graph - The EasyGraph graph used to construct the sparse matrix. - - nodelist : list, optional - The rows and columns are ordered according to the nodes in `nodelist`. - If `nodelist` is None, then the ordering is produced by G.nodes(). - - dtype : NumPy data-type, optional - A valid NumPy dtype used to initialize the array. If None, then the - NumPy default is used. - - weight : string or None optional (default='weight') - The edge attribute that holds the numerical value used for - the edge weight. If None then all edge weights are 1. - - format : str in {'bsr', 'csr', 'csc', 'coo', 'lil', 'dia', 'dok'} - The type of the matrix to be returned (default 'csr'). For - some algorithms different implementations of sparse matrices - can perform better. See [1]_ for details. - - Returns - ------- - A : SciPy sparse array - Graph adjacency matrix. - - Notes - ----- - For directed graphs, matrix entry i,j corresponds to an edge from i to j. - - The matrix entries are populated using the edge attribute held in - parameter weight. When an edge does not have that attribute, the - value of the entry is 1. - - For multiple edges the matrix values are the sums of the edge weights. - - When `nodelist` does not contain every node in `G`, the adjacency matrix - is built from the subgraph of `G` that is induced by the nodes in - `nodelist`. - - The convention used for self-loop edges in graphs is to assign the - diagonal matrix entry value to the weight attribute of the edge - (or the number 1 if the edge has no weight attribute). If the - alternate convention of doubling the edge weight is desired the - resulting Scipy sparse matrix can be modified as follows: - - >>> G = eg.Graph([(1, 1)]) - >>> A = eg.to_scipy_sparse_array(G) - >>> print(A.todense()) - [[1]] - >>> A.setdiag(A.diagonal() * 2) - >>> print(A.toarray()) - [[2]] - - Examples - -------- - >>> S = eg.to_scipy_sparse_array(G, nodelist=[0, 1, 2]) - >>> print(S.toarray()) - [[0 2 0] - [1 0 0] - [0 0 4]] - - References - ---------- - .. [1] Scipy Dev. References, "Sparse Matrices", - https://docs.scipy.org/doc/scipy/reference/sparse.html - """ - import scipy as sp - import scipy.sparse # call as sp.sparse - - if len(G) == 0: - raise eg.EasyGraphError("Graph has no nodes or edges") - - if nodelist is None: - nodelist = list(G) - nlen = len(G) - else: - nlen = len(nodelist) - if nlen == 0: - raise eg.EasyGraphError("nodelist has no nodes") - nodeset = set(G.nbunch_iter(nodelist)) - if nlen != len(nodeset): - for n in nodelist: - if n not in G: - raise eg.EasyGraphError(f"Node {n} in nodelist is not in G") - raise eg.EasyGraphError("nodelist contains duplicates.") - if nlen < len(G): - G = G.subgraph(nodelist) - - index = dict(zip(nodelist, range(nlen))) - - # G.edges(data=weight, default=1) - - coefficients = zip( - *((index[u], index[v], wt.get("weight", 1)) for u, v, wt in G.edges) - ) - try: - row, col, data = coefficients - except ValueError: - # there is no edge in the subgraph - row, col, data = [], [], [] - - if G.is_directed(): - A = sp.sparse.coo_array((data, (row, col)), shape=(nlen, nlen), dtype=dtype) - else: - # symmetrize matrix - d = data + data - r = row + col - c = col + row - # selfloop entries get double counted when symmetrizing - # so we subtract the data on the diagonal - selfloops = list(eg.selfloop_edges(G, data=weight, default=1)) - if selfloops: - diag_index, diag_data = zip(*((index[u], -wt) for u, v, wt in selfloops)) - d += diag_data - r += diag_index - c += diag_index - A = sp.sparse.coo_array((d, (r, c)), shape=(nlen, nlen), dtype=dtype) - try: - return A.asformat(format) - except ValueError as err: - raise eg.EasyGraphError(f"Unknown sparse matrix format: {format}") from err
- - -
[docs]def to_scipy_sparse_matrix(G, nodelist=None, dtype=None, weight="weight", format="csr"): - """Returns the graph adjacency matrix as a SciPy sparse matrix. - - Parameters - ---------- - G : graph - The EasyGraph graph used to construct the sparse matrix. - - nodelist : list, optional - The rows and columns are ordered according to the nodes in `nodelist`. - If `nodelist` is None, then the ordering is produced by G.nodes(). - - dtype : NumPy data-type, optional - A valid NumPy dtype used to initialize the array. If None, then the - NumPy default is used. - - weight : string or None optional (default='weight') - The edge attribute that holds the numerical value used for - the edge weight. If None then all edge weights are 1. - - format : str in {'bsr', 'csr', 'csc', 'coo', 'lil', 'dia', 'dok'} - The type of the matrix to be returned (default 'csr'). For - some algorithms different implementations of sparse matrices - can perform better. See [1]_ for details. - - Returns - ------- - A : SciPy sparse matrix - Graph adjacency matrix. - - Notes - ----- - For directed graphs, matrix entry i,j corresponds to an edge from i to j. - - The matrix entries are populated using the edge attribute held in - parameter weight. When an edge does not have that attribute, the - value of the entry is 1. - - For multiple edges the matrix values are the sums of the edge weights. - - When `nodelist` does not contain every node in `G`, the adjacency matrix - is built from the subgraph of `G` that is induced by the nodes in - `nodelist`. - - The convention used for self-loop edges in graphs is to assign the - diagonal matrix entry value to the weight attribute of the edge - (or the number 1 if the edge has no weight attribute). If the - alternate convention of doubling the edge weight is desired the - resulting Scipy sparse matrix can be modified as follows: - - >>> G = eg.Graph([(1, 1)]) - >>> A = eg.to_scipy_sparse_matrix(G) - >>> print(A.todense()) - [[1]] - >>> A.setdiag(A.diagonal() * 2) - >>> print(A.todense()) - [[2]] - - Examples - -------- - - >>> G.add_edge(1, 0) - 0 - >>> G.add_edge(2, 2, weight=3) - 0 - >>> G.add_edge(2, 2) - 1 - >>> S = eg.to_scipy_sparse_matrix(G, nodelist=[0, 1, 2]) - >>> print(S.todense()) - [[0 2 0] - [1 0 0] - [0 0 4]] - - References - ---------- - .. [1] Scipy Dev. References, "Sparse Matrices", - https://docs.scipy.org/doc/scipy/reference/sparse.html - """ - import scipy as sp - import scipy.sparse - - A = to_scipy_sparse_array( - G, nodelist=nodelist, dtype=dtype, weight=weight, format=format - ) - return sp.sparse.csr_matrix(A).asformat(format)
- - -
[docs]def to_numpy_matrix(G, edge_sign=1.0, not_edge_sign=0.0): - """ - Returns the graph adjacency matrix as a NumPy matrix. - - Parameters - ---------- - edge_sign : float - Sign for the position of matrix where there is an edge - - not_edge_sign : float - Sign for the position of matrix where there is no edge - - """ - import numpy as np - - index_of_node = dict(zip(G.nodes, range(len(G)))) - N = len(G) - M = np.full((N, N), not_edge_sign) - - for u, udict in G.adj.items(): - for v, data in udict.items(): - M[index_of_node[u], index_of_node[v]] = edge_sign - - M = np.asmatrix(M) - return M
- - -
[docs]def from_numpy_array(A, parallel_edges=False, create_using=None): - """Returns a graph from a 2D NumPy array. - - The 2D NumPy array is interpreted as an adjacency matrix for the graph. - - Parameters - ---------- - A : a 2D numpy.ndarray - An adjacency matrix representation of a graph - - parallel_edges : Boolean - If this is True, `create_using` is a multigraph, and `A` is an - integer array, then entry *(i, j)* in the array is interpreted as the - number of parallel edges joining vertices *i* and *j* in the graph. - If it is False, then the entries in the array are interpreted as - the weight of a single edge joining the vertices. - - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - - Notes - ----- - For directed graphs, explicitly mention create_using=eg.DiGraph, - and entry i,j of A corresponds to an edge from i to j. - - If `create_using` is :class:`easygraph.MultiGraph` or - :class:`easygraph.MultiDiGraph`, `parallel_edges` is True, and the - entries of `A` are of type :class:`int`, then this function returns a - multigraph (of the same type as `create_using`) with parallel edges. - - If `create_using` indicates an undirected multigraph, then only the edges - indicated by the upper triangle of the array `A` will be added to the - graph. - - If the NumPy array has a single data type for each array entry it - will be converted to an appropriate Python data type. - - If the NumPy array has a user-specified compound data type the names - of the data fields will be used as attribute keys in the resulting - EasyGraph graph. - - See Also - -------- - to_numpy_array - - Examples - -------- - Simple integer weights on edges: - - >>> import numpy as np - >>> A = np.array([[1, 1], [2, 1]]) - >>> G = eg.from_numpy_array(A) - >>> G.edges(data=True) - EdgeDataView([(0, 0, {'weight': 1}), (0, 1, {'weight': 2}), (1, 1, {'weight': 1})]) - - If `create_using` indicates a multigraph and the array has only integer - entries and `parallel_edges` is False, then the entries will be treated - as weights for edges joining the nodes (without creating parallel edges): - - >>> A = np.array([[1, 1], [1, 2]]) - >>> G = eg.from_numpy_array(A, create_using=eg.MultiGraph) - >>> G[1][1] - AtlasView({0: {'weight': 2}}) - - If `create_using` indicates a multigraph and the array has only integer - entries and `parallel_edges` is True, then the entries will be treated - as the number of parallel edges joining those two vertices: - - >>> A = np.array([[1, 1], [1, 2]]) - >>> temp = eg.MultiGraph() - >>> G = eg.from_numpy_array(A, parallel_edges=True, create_using=temp) - >>> G[1][1] - AtlasView({0: {'weight': 1}, 1: {'weight': 1}}) - - User defined compound data type on edges: - - >>> dt = [("weight", float), ("cost", int)] - >>> A = np.array([[(1.0, 2)]], dtype=dt) - >>> G = eg.from_numpy_array(A) - >>> G.edges() - EdgeView([(0, 0)]) - >>> G[0][0]["cost"] - 2 - >>> G[0][0]["weight"] - 1.0 - - """ - kind_to_python_type = { - "f": float, - "i": int, - "u": int, - "b": bool, - "c": complex, - "S": str, - "U": str, - "V": "void", - } - G = eg.empty_graph(0, create_using) - if A.ndim != 2: - raise eg.EasyGraphError(f"Input array must be 2D, not {A.ndim}") - n, m = A.shape - if n != m: - raise eg.EasyGraphError(f"Adjacency matrix not square: eg,ny={A.shape}") - dt = A.dtype - try: - python_type = kind_to_python_type[dt.kind] - except Exception as err: - raise TypeError(f"Unknown numpy data type: {dt}") from err - - # Make sure we get even the isolated nodes of the graph. - G.add_nodes_from(range(n)) - # Get a list of all the entries in the array with nonzero entries. These - # coordinates become edges in the graph. (convert to int from np.int64) - edges = ((int(e[0]), int(e[1])) for e in zip(*A.nonzero())) - # handle numpy constructed data type - if python_type == "void": - # Sort the fields by their offset, then by dtype, then by name. - fields = sorted( - (offset, dtype, name) for name, (dtype, offset) in A.dtype.fields.items() - ) - triples = ( - ( - u, - v, - { - name: kind_to_python_type[dtype.kind](val) - for (_, dtype, name), val in zip(fields, A[u, v]) - }, - ) - for u, v in edges - ) - # If the entries in the adjacency matrix are integers, the graph is a - # multigraph, and parallel_edges is True, then create parallel edges, each - # with weight 1, for each entry in the adjacency matrix. Otherwise, create - # one edge for each positive entry in the adjacency matrix and set the - # weight of that edge to be the entry in the matrix. - elif python_type is int and G.is_multigraph() and parallel_edges: - chain = itertools.chain.from_iterable - # The following line is equivalent to: - # - # for (u, v) in edges: - # for d in range(A[u, v]): - # G.add_edge(u, v, weight=1) - # - triples = chain( - ((u, v, {"weight": 1}) for d in range(A[u, v])) for (u, v) in edges - ) - else: # basic data type - triples = ((u, v, dict(weight=python_type(A[u, v]))) for u, v in edges) - # If we are creating an undirected multigraph, only add the edges from the - # upper triangle of the matrix. Otherwise, add all the edges. This relies - # on the fact that the vertices created in the - # `_generated_weighted_edges()` function are actually the row/column - # indices for the matrix `A`. - # - # Without this check, we run into a problem where each edge is added twice - # when `G.add_edges_from()` is invoked below. - if G.is_multigraph() and not G.is_directed(): - triples = ((u, v, d) for u, v, d in triples if u <= v) - G.add_edges_from(triples) - return G
- - -
[docs]def to_numpy_array( - G, - nodelist=None, - dtype=None, - order=None, - multigraph_weight=sum, - weight="weight", - nonedge=0.0, -): - """Returns the graph adjacency matrix as a NumPy array. - - Parameters - ---------- - G : graph - The EasyGraph graph used to construct the NumPy array. - - nodelist : list, optional - The rows and columns are ordered according to the nodes in `nodelist`. - If `nodelist` is None, then the ordering is produced by G.nodes(). - - dtype : NumPy data type, optional - A valid single NumPy data type used to initialize the array. - This must be a simple type such as int or numpy.float64 and - not a compound data type (see to_numpy_recarray) - If None, then the NumPy default is used. - - order : {'C', 'F'}, optional - Whether to store multidimensional data in C- or Fortran-contiguous - (row- or column-wise) order in memory. If None, then the NumPy default - is used. - - multigraph_weight : {sum, min, max}, optional - An operator that determines how weights in multigraphs are handled. - The default is to sum the weights of the multiple edges. - - weight : string or None optional (default = 'weight') - The edge attribute that holds the numerical value used for - the edge weight. If an edge does not have that attribute, then the - value 1 is used instead. - - nonedge : float (default = 0.0) - The array values corresponding to nonedges are typically set to zero. - However, this could be undesirable if there are array values - corresponding to actual edges that also have the value zero. If so, - one might prefer nonedges to have some other value, such as nan. - - Returns - ------- - A : NumPy ndarray - Graph adjacency matrix - - See Also - -------- - from_numpy_array - - Notes - ----- - For directed graphs, entry i,j corresponds to an edge from i to j. - - Entries in the adjacency matrix are assigned to the weight edge attribute. - When an edge does not have a weight attribute, the value of the entry is - set to the number 1. For multiple (parallel) edges, the values of the - entries are determined by the `multigraph_weight` parameter. The default is - to sum the weight attributes for each of the parallel edges. - - When `nodelist` does not contain every node in `G`, the adjacency matrix is - built from the subgraph of `G` that is induced by the nodes in `nodelist`. - - The convention used for self-loop edges in graphs is to assign the - diagonal array entry value to the weight attribute of the edge - (or the number 1 if the edge has no weight attribute). If the - alternate convention of doubling the edge weight is desired the - resulting NumPy array can be modified as follows: - - >>> import numpy as np - >>> G = eg.Graph([(1, 1)]) - >>> A = eg.to_numpy_array(G) - >>> A - array([[1.]]) - >>> A[np.diag_indices_from(A)] *= 2 - >>> A - array([[2.]]) - - Examples - -------- - >>> G = eg.MultiDiGraph() - >>> G.add_edge(0, 1, weight=2) - 0 - >>> G.add_edge(1, 0) - 0 - >>> G.add_edge(2, 2, weight=3) - 0 - >>> G.add_edge(2, 2) - 1 - >>> eg.to_numpy_array(G, nodelist=[0, 1, 2]) - array([[0., 2., 0.], - [1., 0., 0.], - [0., 0., 4.]]) - - """ - import numpy as np - - if nodelist is None: - nodelist = list(G) - nodeset = G - nlen = len(G) - else: - nlen = len(nodelist) - nodeset = set(G.nodes) - if nlen != len(nodeset): - for n in nodelist: - if n not in G: - raise eg.EasyGraphError(f"Node {n} in nodelist is not in G") - raise eg.EasyGraphError("nodelist contains duplicates.") - - undirected = not G.is_directed() - index = dict(zip(nodelist, range(nlen))) - - # Initially, we start with an array of nans. Then we populate the array - # using data from the graph. Afterwards, any leftover nans will be - # converted to the value of `nonedge`. Note, we use nans initially, - # instead of zero, for two reasons: - # - # 1) It can be important to distinguish a real edge with the value 0 - # from a nonedge with the value 0. - # - # 2) When working with multi(di)graphs, we must combine the values of all - # edges between any two nodes in some manner. This often takes the - # form of a sum, min, or max. Using the value 0 for a nonedge would - # have undesirable effects with min and max, but using nanmin and - # nanmax with initially nan values is not problematic at all. - # - # That said, there are still some drawbacks to this approach. Namely, if - # a real edge is nan, then that value is a) not distinguishable from - # nonedges and b) is ignored by the default combinator (nansum, nanmin, - # nanmax) functions used for multi(di)graphs. If this becomes an issue, - # an alternative approach is to use masked arrays. Initially, every - # element is masked and set to some `initial` value. As we populate the - # graph, elements are unmasked (automatically) when we combine the initial - # value with the values given by real edges. At the end, we convert all - # masked values to `nonedge`. Using masked arrays fully addresses reason 1, - # but for reason 2, we would still have the issue with min and max if the - # initial values were 0.0. Note: an initial value of +inf is appropriate - # for min, while an initial value of -inf is appropriate for max. When - # working with sum, an initial value of zero is appropriate. Ideally then, - # we'd want to allow users to specify both a value for nonedges and also - # an initial value. For multi(di)graphs, the choice of the initial value - # will, in general, depend on the combinator function---sensible defaults - # can be provided. - - if G.is_multigraph(): - # Handle MultiGraphs and MultiDiGraphs - A = np.full((nlen, nlen), np.nan, order=order) - # use numpy nan-aware operations - operator = {sum: np.nansum, min: np.nanmin, max: np.nanmax} - try: - op = operator[multigraph_weight] - except Exception as err: - raise ValueError("multigraph_weight must be sum, min, or max") from err - - for u, v, _, attrs in G.edges: - if (u in nodeset) and (v in nodeset): - i, j = index[u], index[v] - e_weight = attrs.get(weight, 1) - A[i, j] = op([e_weight, A[i, j]]) - if undirected: - A[j, i] = A[i, j] - else: - # Graph or DiGraph, this is much faster than above - A = np.full((nlen, nlen), np.nan, order=order) - for u, nbrdict in G.adj.items(): - for v, d in nbrdict.items(): - try: - A[index[u], index[v]] = d.get(weight, 1) - except KeyError: - # This occurs when there are fewer desired nodes than - # there are nodes in the graph: len(nodelist) < len(G) - pass - - A[np.isnan(A)] = nonedge - A = np.asarray(A, dtype=dtype) - return A
- - -
[docs]def from_pandas_adjacency(df, create_using=None): - r"""Returns a graph from Pandas DataFrame. - - The Pandas DataFrame is interpreted as an adjacency matrix for the graph. - - Parameters - ---------- - df : Pandas DataFrame - An adjacency matrix representation of a graph - - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - - Notes - ----- - For directed graphs, explicitly mention create_using=eg.DiGraph, - and entry i,j of df corresponds to an edge from i to j. - - If `df` has a single data type for each entry it will be converted to an - appropriate Python data type. - - If `df` has a user-specified compound data type the names - of the data fields will be used as attribute keys in the resulting - EasyGraph graph. - - See Also - -------- - to_pandas_adjacency - - Examples - -------- - Simple integer weights on edges: - - >>> import pandas as pd - >>> pd.options.display.max_columns = 20 - >>> df = pd.DataFrame([[1, 1], [2, 1]]) - >>> df - 0 1 - 0 1 1 - 1 2 1 - >>> G = eg.from_pandas_adjacency(df) - >>> G.name = "Graph from pandas adjacency matrix" - """ - - try: - df = df[df.index] - except Exception as err: - missing = list(set(df.index).difference(set(df.columns))) - msg = f"{missing} not in columns" - raise eg.EasyGraphError("Columns must match Indices.", msg) from err - - A = df.values - G = from_numpy_array(A, create_using=create_using) - - G = eg.relabel_nodes(G, dict(enumerate(df.columns))) - return G
- - -
[docs]def from_pandas_edgelist( - df, - source="source", - target="target", - edge_attr=None, - create_using=None, - edge_key=None, -): - """Returns a graph from Pandas DataFrame containing an edge list. - - The Pandas DataFrame should contain at least two columns of node names and - zero or more columns of edge attributes. Each row will be processed as one - edge instance. - - Note: This function iterates over DataFrame.values, which is not - guaranteed to retain the data type across columns in the row. This is only - a problem if your row is entirely numeric and a mix of ints and floats. In - that case, all values will be returned as floats. See the - DataFrame.iterrows documentation for an example. - - Parameters - ---------- - df : Pandas DataFrame - An edge list representation of a graph - - source : str or int - A valid column name (string or integer) for the source nodes (for the - directed case). - - target : str or int - A valid column name (string or integer) for the target nodes (for the - directed case). - - edge_attr : str or int, iterable, True, or None - A valid column name (str or int) or iterable of column names that are - used to retrieve items and add them to the graph as edge attributes. - If `True`, all of the remaining columns will be added. - If `None`, no edge attributes are added to the graph. - - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - - edge_key : str or None, optional (default=None) - A valid column name for the edge keys (for a MultiGraph). The values in - this column are used for the edge keys when adding edges if create_using - is a multigraph. - - See Also - -------- - to_pandas_edgelist - - Examples - -------- - Simple integer weights on edges: - - >>> import pandas as pd - >>> pd.options.display.max_columns = 20 - >>> import numpy as np - >>> rng = np.random.RandomState(seed=5) - >>> ints = 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 - >>> df[["weight", "cost", 0, "b"]] - weight cost 0 b - 0 4 7 A D - 1 7 1 B A - 2 10 9 C E - >>> G = eg.from_pandas_edgelist(df, 0, "b", ["weight", "cost"]) - >>> G["E"]["C"]["weight"] - 10 - >>> G["E"]["C"]["cost"] - 9 - >>> edges = pd.DataFrame( - ... { - ... "source": [0, 1, 2], - ... "target": [2, 2, 3], - ... "weight": [3, 4, 5], - ... "color": ["red", "blue", "blue"], - ... } - ... ) - >>> G = eg.from_pandas_edgelist(edges, edge_attr=True) - >>> G[0][2]["color"] - 'red' - - Build multigraph with custom keys: - - >>> edges = pd.DataFrame( - ... { - ... "source": [0, 1, 2, 0], - ... "target": [2, 2, 3, 2], - ... "my_edge_key": ["A", "B", "C", "D"], - ... "weight": [3, 4, 5, 6], - ... "color": ["red", "blue", "blue", "blue"], - ... } - ... ) - >>> G = eg.from_pandas_edgelist( - ... edges, - ... edge_key="my_edge_key", - ... edge_attr=["weight", "color"], - ... create_using=eg.MultiGraph(), - ... ) - >>> G[0][2] - AtlasView({'A': {'weight': 3, 'color': 'red'}, 'D': {'weight': 6, 'color': 'blue'}}) - - - """ - g = eg.empty_graph(0, create_using) - - if edge_attr is None: - g.add_edges_from(zip(df[source], df[target])) - return g - - reserved_columns = [source, target] - - # Additional columns requested - attr_col_headings = [] - attribute_data = [] - if edge_attr is True: - attr_col_headings = [c for c in df.columns if c not in reserved_columns] - elif isinstance(edge_attr, (list, tuple)): - attr_col_headings = edge_attr - else: - attr_col_headings = [edge_attr] - if len(attr_col_headings) == 0: - raise eg.EasyGraphError( - "Invalid edge_attr argument: No columns found with name:" - f" {attr_col_headings}" - ) - - try: - attribute_data = zip(*[df[col] for col in attr_col_headings]) - except (KeyError, TypeError) as err: - msg = f"Invalid edge_attr argument: {edge_attr}" - raise eg.EasyGraphError(msg) from err - - if g.is_multigraph(): - # => append the edge keys from the df to the bundled data - if edge_key is not None: - try: - multigraph_edge_keys = df[edge_key] - attribute_data = zip(attribute_data, multigraph_edge_keys) - except (KeyError, TypeError) as err: - msg = f"Invalid edge_key argument: {edge_key}" - raise eg.EasyGraphError(msg) from err - - for s, t, attrs in zip(df[source], df[target], attribute_data): - if edge_key is not None: - attrs, multigraph_edge_key = attrs - key = g.add_edge(s, t, key=multigraph_edge_key) - else: - key = g.add_edge(s, t) - - g[s][t][key].update(zip(attr_col_headings, attrs)) - else: - for s, t, attrs in zip(df[source], df[target], attribute_data): - g.add_edge(s, t) - g[s][t].update(zip(attr_col_headings, attrs)) - - return g
- - -
[docs]def from_scipy_sparse_matrix( - A, parallel_edges=False, create_using=None, edge_attribute="weight" -): - """Creates a new graph from an adjacency matrix given as a SciPy sparse - matrix. - - Parameters - ---------- - A: scipy sparse matrix - An adjacency matrix representation of a graph - - parallel_edges : Boolean - If this is True, `create_using` is a multigraph, and `A` is an - integer matrix, then entry *(i, j)* in the matrix is interpreted as the - number of parallel edges joining vertices *i* and *j* in the graph. - If it is False, then the entries in the matrix are interpreted as - the weight of a single edge joining the vertices. - - create_using : EasyGraph graph constructor, optional (default=eg.Graph) - Graph type to create. If graph instance, then cleared before populated. - - edge_attribute: string - Name of edge attribute to store matrix numeric value. The data will - have the same type as the matrix entry (int, float, (real,imag)). - - Notes - ----- - For directed graphs, explicitly mention create_using=eg.DiGraph, - and entry i,j of A corresponds to an edge from i to j. - - If `create_using` is :class:`easygraph.MultiGraph` or - :class:`easygraph.MultiDiGraph`, `parallel_edges` is True, and the - entries of `A` are of type :class:`int`, then this function returns a - multigraph (constructed from `create_using`) with parallel edges. - In this case, `edge_attribute` will be ignored. - - If `create_using` indicates an undirected multigraph, then only the edges - indicated by the upper triangle of the matrix `A` will be added to the - graph. - - Examples - -------- - >>> import scipy as sp - >>> import scipy.sparse # call as sp.sparse - >>> A = sp.sparse.eye(2, 2, 1) - >>> G = eg.from_scipy_sparse_matrix(A) - - If `create_using` indicates a multigraph and the matrix has only integer - entries and `parallel_edges` is Falnxse, then the entries will be treated - as weights for edges joining the nodes (without creating parallel edges): - - >>> A = sp.sparse.csr_matrix([[1, 1], [1, 2]]) - >>> G = eg.from_scipy_sparse_matrix(A, create_using=eg.MultiGraph) - >>> G[1][1] - AtlasView({0: {'weight': 2}}) - - If `create_using` indicates a multigraph and the matrix has only integer - entries and `parallel_edges` is True, then the entries will be treated - as the number of parallel edges joining those two vertices: - - >>> A = sp.sparse.csr_matrix([[1, 1], [1, 2]]) - >>> G = eg.from_scipy_sparse_matrix( - ... A, parallel_edges=True, create_using=eg.MultiGraph - ... ) - >>> G[1][1] - AtlasView({0: {'weight': 1}, 1: {'weight': 1}}) - - """ - - return from_scipy_sparse_array( - A, - parallel_edges=parallel_edges, - create_using=create_using, - edge_attribute=edge_attribute, - )
- - -def from_scipy_sparse_array( - A, parallel_edges=False, create_using=None, edge_attribute="weight" -): - G = eg.empty_graph(0, create_using) - n, m = A.shape - if n != m: - raise eg.EasyGraphError(f"Adjacency matrix not square: nx,ny={A.shape}") - # Make sure we get even the isolated nodes of the graph. - G.add_nodes_from(range(n)) - # Create an iterable over (u, v, w) triples and for each triple, add an - # edge from u to v with weight w. - triples = _generate_weighted_edges(A) - # If the entries in the adjacency matrix are integers, the graph is a - # multigraph, and parallel_edges is True, then create parallel edges, each - # with weight 1, for each entry in the adjacency matrix. Otherwise, create - # one edge for each positive entry in the adjacency matrix and set the - # weight of that edge to be the entry in the matrix. - if A.dtype.kind in ("i", "u") and G.is_multigraph() and parallel_edges: - chain = itertools.chain.from_iterable - # The following line is equivalent to: - # - # for (u, v) in edges: - # for d in range(A[u, v]): - # G.add_edge(u, v, weight=1) - # - triples = chain(((u, v, 1) for d in range(w)) for (u, v, w) in triples) - # If we are creating an undirected multigraph, only add the edges from the - # upper triangle of the matrix. Otherwise, add all the edges. This relies - # on the fact that the vertices created in the - # `_generated_weighted_edges()` function are actually the row/column - # indices for the matrix `A`. - # - # Without this check, we run into a problem where each edge is added twice - # when `G.add_weighted_edges_from()` is invoked below. - if G.is_multigraph() and not G.is_directed(): - triples = ((u, v, d) for u, v, d in triples if u <= v) - G.add_edges_from(((u, v, {"weight": d}) for u, v, d in triples)) - return G - - -def _generate_weighted_edges(A): - """Returns an iterable over (u, v, w) triples, where u and v are adjacent - vertices and w is the weight of the edge joining u and v. - - `A` is a SciPy sparse matrix (in any format). - - """ - if A.format == "csr": - return _csr_gen_triples(A) - if A.format == "csc": - return _csc_gen_triples(A) - if A.format == "dok": - return _dok_gen_triples(A) - # If A is in any other format (including COO), convert it to COO format. - return _coo_gen_triples(A.tocoo()) - - -def _csr_gen_triples(A): - """Converts a SciPy sparse matrix in **Compressed Sparse Row** format to - an iterable of weighted edge triples. - - """ - nrows = A.shape[0] - data, indices, indptr = A.data, A.indices, A.indptr - for i in range(nrows): - for j in range(indptr[i], indptr[i + 1]): - yield i, indices[j], data[j] - - -def _csc_gen_triples(A): - """Converts a SciPy sparse matrix in **Compressed Sparse Column** format to - an iterable of weighted edge triples. - - """ - ncols = A.shape[1] - data, indices, indptr = A.data, A.indices, A.indptr - for i in range(ncols): - for j in range(indptr[i], indptr[i + 1]): - yield indices[j], i, data[j] - - -def _coo_gen_triples(A): - """Converts a SciPy sparse matrix in **Coordinate** format to an iterable - of weighted edge triples. - - """ - row, col, data = A.row, A.col, A.data - return zip(row, col, data) - - -def _dok_gen_triples(A): - """Converts a SciPy sparse matrix in **Dictionary of Keys** format to an - iterable of weighted edge triples. - - """ - for (r, c), v in A.items(): - yield r, c, v -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/decorators.html b/docs/_modules/easygraph/utils/decorators.html deleted file mode 100644 index c16b34ed..00000000 --- a/docs/_modules/easygraph/utils/decorators.html +++ /dev/null @@ -1,1258 +0,0 @@ - - - - - - easygraph.utils.decorators — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.decorators

-import bz2
-import collections
-import gzip
-import inspect
-import re
-
-from collections import defaultdict
-from functools import wraps
-from os.path import splitext
-from pathlib import Path
-
-import easygraph as eg
-
-from easygraph.utils.exception import EasyGraphError
-
-
-__all__ = [
-    "only_implemented_for_UnDirected_graph",
-    "only_implemented_for_Directed_graph",
-    "open_file",
-    "nodes_or_number",
-    "not_implemented_for",
-    "hybrid",
-    "retry_method_with_fix",
-]
-
-
-
[docs]def retry_method_with_fix(fix_method): - """Decorator that executes a fix method before retrying again when the decorated method - fails once with any exception. - - If the decorated method fails again, the execution fails with that exception. - - Notes - ----- - This decorator only works on class methods, and the fix function must also be a class method. - It would not work on functions. - - Parameters - ---------- - fix_func : callable - The fix method to execute. It should not accept any arguments. Its return values are - ignored. - """ - - def _creator(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - # pylint: disable=W0703,bare-except - try: - return func(self, *args, **kwargs) - except: - fix_method(self) - return func(self, *args, **kwargs) - - return wrapper - - return _creator
- - -
[docs]def only_implemented_for_UnDirected_graph(func): - # print("--------{:<40}: Only Implemented For UnDirected Graph--------".format(func.__name__)) - return func
- - -
[docs]def only_implemented_for_Directed_graph(func): - # print("--------{:<40}: Only Implemented For Directed Graph--------".format(func.__name__)) - return func
- - -
[docs]def not_implemented_for(*graph_types): - """Decorator to mark algorithms as not implemented - - Parameters - ---------- - graph_types : container of strings - Entries must be one of "directed", "undirected", "multigraph", or "graph". - - Returns - ------- - _require : function - The decorated function. - - Raises - ------ - EasyGraphNotImplemented - If any of the packages cannot be imported - - Notes - ----- - Multiple types are joined logically with "and". - For "or" use multiple @not_implemented_for() lines. - - Examples - -------- - Decorate functions like this:: - - @not_implemented_for("directed") - def sp_function(G): - pass - - # rule out MultiDiGraph - @not_implemented_for("directed","multigraph") - def sp_np_function(G): - pass - - # rule out all except DiGraph - @not_implemented_for("undirected") - @not_implemented_for("multigraph") - def sp_np_function(G): - pass - """ - if ("directed" in graph_types) and ("undirected" in graph_types): - raise ValueError("Function not implemented on directed AND undirected graphs?") - if ("multigraph" in graph_types) and ("graph" in graph_types): - raise ValueError("Function not implemented on graph AND multigraphs?") - if not set(graph_types) < {"directed", "undirected", "multigraph", "graph"}: - raise KeyError( - "use one or more of directed, undirected, multigraph, graph. " - f"You used {graph_types}" - ) - - # 3-way logic: True if "directed" input, False if "undirected" input, else None - dval = ("directed" in graph_types) or not ("undirected" in graph_types) and None - mval = ("multigraph" in graph_types) or not ("graph" in graph_types) and None - errmsg = f"not implemented for {' '.join(graph_types)} type" - - def _not_implemented_for(g): - if (mval is None or mval == g.is_multigraph()) and ( - dval is None or dval == g.is_directed() - ): - raise eg.EasyGraphNotImplemented(errmsg) - - return g - - return argmap(_not_implemented_for, 0)
- - -import functools - -import cpp_easygraph - - -
[docs]def hybrid(cpp_method_name): - def _hybrid(py_method): - @functools.wraps(py_method) - def method(*args, **kwargs): - G = args[0] - if G.cflag and cpp_method_name is not None: - try: - cpp_method = getattr(cpp_easygraph, cpp_method_name) - return cpp_method(*args, **kwargs) - except AttributeError as e: - print(f"Warning: {e}. Use python method instead.") - return py_method(*args, **kwargs) - - return method - - return _hybrid
- - -# To handle new extensions, define a function accepting a `path` and `mode`. -# Then add the extension to _dispatch_dict. -fopeners = { - ".gz": gzip.open, - ".gzip": gzip.open, - ".bz2": bz2.BZ2File, -} -_dispatch_dict = defaultdict(lambda: open, **fopeners) # type: ignore - - -
[docs]def open_file(path_arg, mode="r"): - """Decorator to ensure clean opening and closing of files. - - Parameters - ---------- - path_arg : string or int - Name or index of the argument that is a path. - - mode : str - String for opening mode. - - Returns - ------- - _open_file : function - Function which cleanly executes the io. - - Examples - -------- - Decorate functions like this:: - - @open_file(0,"r") - def read_function(pathname): - pass - - @open_file(1,"w") - def write_function(G, pathname): - pass - - @open_file(1,"w") - def write_function(G, pathname="graph.dot"): - pass - - @open_file("pathname","w") - def write_function(G, pathname="graph.dot"): - pass - - @open_file("path", "w+") - def another_function(arg, **kwargs): - path = kwargs["path"] - pass - - Notes - ----- - Note that this decorator solves the problem when a path argument is - specified as a string, but it does not handle the situation when the - function wants to accept a default of None (and then handle it). - - Here is an example of how to handle this case:: - - @open_file("path") - def some_function(arg1, arg2, path=None): - if path is None: - fobj = tempfile.NamedTemporaryFile(delete=False) - else: - # `path` could have been a string or file object or something - # similar. In any event, the decorator has given us a file object - # and it will close it for us, if it should. - fobj = path - - try: - fobj.write("blah") - finally: - if path is None: - fobj.close() - - Normally, we'd want to use "with" to ensure that fobj gets closed. - However, the decorator will make `path` a file object for us, - and using "with" would undesirably close that file object. - Instead, we use a try block, as shown above. - When we exit the function, fobj will be closed, if it should be, by the decorator. - """ - - def _open_file(path): - # Now we have the path_arg. There are two types of input to consider: - # 1) string representing a path that should be opened - # 2) an already opened file object - if isinstance(path, str): - ext = splitext(path)[1] - elif isinstance(path, Path): - # path is a pathlib reference to a filename - ext = path.suffix - path = str(path) - else: - # could be None, or a file handle, in which case the algorithm will deal with it - return path, lambda: None - - fobj = _dispatch_dict[ext](path, mode=mode) - return fobj, lambda: fobj.close() - - return argmap(_open_file, path_arg, try_finally=True)
- - -class argmap: - """A decorator to apply a map to arguments before calling the function - - This class provides a decorator that maps (transforms) arguments of the function - before the function is called. Thus for example, we have similar code - in many functions to determine whether an argument is the number of nodes - to be created, or a list of nodes to be handled. The decorator provides - the code to accept either -- transforming the indicated argument into a - list of nodes before the actual function is called. - - This decorator class allows us to process single or multiple arguments. - The arguments to be processed can be specified by string, naming the argument, - or by index, specifying the item in the args list. - - Parameters - ---------- - func : callable - The function to apply to arguments - - *args : iterable of (int, str or tuple) - A list of parameters, specified either as strings (their names), ints - (numerical indices) or tuples, which may contain ints, strings, and - (recursively) tuples. Each indicates which parameters the decorator - should map. Tuples indicate that the map function takes (and returns) - multiple parameters in the same order and nested structure as indicated - here. - - try_finally : bool (default: False) - When True, wrap the function call in a try-finally block with code - for the finally block created by `func`. This is used when the map - function constructs an object (like a file handle) that requires - post-processing (like closing). - - Examples - -------- - Most of these examples use `@argmap(...)` to apply the decorator to - the function defined on the next line. - In the EasyGraph codebase however, `argmap` is used within a function to - construct a decorator. That is, the decorator defines a mapping function - and then uses `argmap` to build and return a decorated function. - A simple example is a decorator that specifies which currency to report money. - The decorator (named `convert_to`) would be used like:: - - @convert_to("US_Dollars", "income") - def show_me_the_money(name, income): - print(f"{name} : {income}") - - And the code to create the decorator might be:: - - def convert_to(currency, which_arg): - def _convert(amount): - if amount.currency != currency: - amount = amount.to_currency(currency) - return amount - return argmap(_convert, which_arg) - - Despite this common idiom for argmap, most of the following examples - use the `@argmap(...)` idiom to save space. - - Here's an example use of argmap to sum the elements of two of the functions - arguments. The decorated function:: - - @argmap(sum, "xlist", "zlist") - def foo(xlist, y, zlist): - return xlist - y + zlist - - is syntactic sugar for:: - - def foo(xlist, y, zlist): - x = sum(xlist) - z = sum(zlist) - return x - y + z - - and is equivalent to (using argument indexes):: - - @argmap(sum, "xlist", 2) - def foo(xlist, y, zlist): - return xlist - y + zlist - - or:: - - @argmap(sum, "zlist", 0) - def foo(xlist, y, zlist): - return xlist - y + zlist - - Transforming functions can be applied to multiple arguments, such as:: - - def swap(x, y): - return y, x - - # the 2-tuple tells argmap that the map `swap` has 2 inputs/outputs. - @argmap(swap, ("a", "b")): - def foo(a, b, c): - return a / b * c - - is equivalent to:: - - def foo(a, b, c): - a, b = swap(a, b) - return a / b * c - - More generally, the applied arguments can be nested tuples of strings or ints. - The syntax `@argmap(some_func, ("a", ("b", "c")))` would expect `some_func` to - accept 2 inputs with the second expected to be a 2-tuple. It should then return - 2 outputs with the second a 2-tuple. The returns values would replace input "a" - "b" and "c" respectively. Similarly for `@argmap(some_func, (0, ("b", 2)))`. - - Also, note that an index larger than the number of named parameters is allowed - for variadic functions. For example:: - - def double(a): - return 2 * a - - @argmap(double, 3) - def overflow(a, *args): - return a, args - - print(overflow(1, 2, 3, 4, 5, 6)) # output is 1, (2, 3, 8, 5, 6) - - **Try Finally** - - Additionally, this `argmap` class can be used to create a decorator that - initiates a try...finally block. The decorator must be written to return - both the transformed argument and a closing function. - This feature was included to enable the `open_file` decorator which might - need to close the file or not depending on whether it had to open that file. - This feature uses the keyword-only `try_finally` argument to `@argmap`. - - For example this map opens a file and then makes sure it is closed:: - - def open_file(fn): - f = open(fn) - return f, lambda: f.close() - - The decorator applies that to the function `foo`:: - - @argmap(open_file, "file", try_finally=True) - def foo(file): - print(file.read()) - - is syntactic sugar for:: - - def foo(file): - file, close_file = open_file(file) - try: - print(file.read()) - finally: - close_file() - - and is equivalent to (using indexes):: - - @argmap(open_file, 0, try_finally=True) - def foo(file): - print(file.read()) - - Here's an example of the try_finally feature used to create a decorator:: - - def my_closing_decorator(which_arg): - def _opener(path): - if path is None: - path = open(path) - fclose = path.close - else: - # assume `path` handles the closing - fclose = lambda: None - return path, fclose - return argmap(_opener, which_arg, try_finally=True) - - which can then be used as:: - - @my_closing_decorator("file") - def fancy_reader(file=None): - # this code doesn't need to worry about closing the file - print(file.read()) - - Notes - ----- - An object of this class is callable and intended to be used when - defining a decorator. Generally, a decorator takes a function as input - and constructs a function as output. Specifically, an `argmap` object - returns the input function decorated/wrapped so that specified arguments - are mapped (transformed) to new values before the decorated function is called. - - As an overview, the argmap object returns a new function with all the - dunder values of the original function (like `__doc__`, `__name__`, etc). - Code for this decorated function is built based on the original function's - signature. It starts by mapping the input arguments to potentially new - values. Then it calls the decorated function with these new values in place - of the indicated arguments that have been mapped. The return value of the - original function is then returned. This new function is the function that - is actually called by the user. - - Three additional features are provided. - 1) The code is lazily compiled. That is, the new function is returned - as an object without the code compiled, but with all information - needed so it can be compiled upon it's first invocation. This saves - time on import at the cost of additional time on the first call of - the function. Subsequent calls are then just as fast as normal. - - 2) If the "try_finally" keyword-only argument is True, a try block - follows each mapped argument, matched on the other side of the wrapped - call, by a finally block closing that mapping. We expect func to return - a 2-tuple: the mapped value and a function to be called in the finally - clause. This feature was included so the `open_file` decorator could - provide a file handle to the decorated function and close the file handle - after the function call. It even keeps track of whether to close the file - handle or not based on whether it had to open the file or the input was - already open. So, the decorated function does not need to include any - code to open or close files. - - 3) The maps applied can process multiple arguments. For example, - you could swap two arguments using a mapping, or transform - them to their sum and their difference. This was included to allow - a decorator in the `quality.py` module that checks that an input - `partition` is a valid partition of the nodes of the input graph `G`. - In this example, the map has inputs `(G, partition)`. After checking - for a valid partition, the map either raises an exception or leaves - the inputs unchanged. Thus many functions that make this check can - use the decorator rather than copy the checking code into each function. - More complicated nested argument structures are described below. - - The remaining notes describe the code structure and methods for this - class in broad terms to aid in understanding how to use it. - - Instantiating an `argmap` object simply stores the mapping function and - the input identifiers of which arguments to map. The resulting decorator - is ready to use this map to decorate any function. Calling that object - (`argmap.__call__`, but usually done via `@my_decorator`) a lazily - compiled thin wrapper of the decorated function is constructed, - wrapped with the necessary function dunder attributes like `__doc__` - and `__name__`. That thinly wrapped function is returned as the - decorated function. When that decorated function is called, the thin - wrapper of code calls `argmap._lazy_compile` which compiles the decorated - function (using `argmap.compile`) and replaces the code of the thin - wrapper with the newly compiled code. This saves the compilation step - every import of easygraph, at the cost of compiling upon the first call - to the decorated function. - - When the decorated function is compiled, the code is recursively assembled - using the `argmap.assemble` method. The recursive nature is needed in - case of nested decorators. The result of the assembly is a number of - useful objects. - - sig : the function signature of the original decorated function as - constructed by :func:`argmap.signature`. This is constructed - using `inspect.signature` but enhanced with attribute - strings `sig_def` and `sig_call`, and other information - specific to mapping arguments of this function. - This information is used to construct a string of code defining - the new decorated function. - - wrapped_name : a unique internally used name constructed by argmap - for the decorated function. - - functions : a dict of the functions used inside the code of this - decorated function, to be used as `globals` in `exec`. - This dict is recursively updated to allow for nested decorating. - - mapblock : code (as a list of strings) to map the incoming argument - values to their mapped values. - - finallys : code (as a list of strings) to provide the possibly nested - set of finally clauses if needed. - - mutable_args : a bool indicating whether the `sig.args` tuple should be - converted to a list so mutation can occur. - - After this recursive assembly process, the `argmap.compile` method - constructs code (as strings) to convert the tuple `sig.args` to a list - if needed. It joins the defining code with appropriate indents and - compiles the result. Finally, this code is evaluated and the original - wrapper's implementation is replaced with the compiled version (see - `argmap._lazy_compile` for more details). - - Other `argmap` methods include `_name` and `_count` which allow internally - generated names to be unique within a python session. - The methods `_flatten` and `_indent` process the nested lists of strings - into properly indented python code ready to be compiled. - - More complicated nested tuples of arguments also allowed though - usually not used. For the simple 2 argument case, the argmap - input ("a", "b") implies the mapping function will take 2 arguments - and return a 2-tuple of mapped values. A more complicated example - with argmap input `("a", ("b", "c"))` requires the mapping function - take 2 inputs, with the second being a 2-tuple. It then must output - the 3 mapped values in the same nested structure `(newa, (newb, newc))`. - This level of generality is not often needed, but was convenient - to implement when handling the multiple arguments. - - See Also - -------- - not_implemented_for - open_file - nodes_or_number - random_state - py_random_state - easygraph.community.quality.require_partition - require_partition - - """ - - def __init__(self, func, *args, try_finally=False): - self._func = func - self._args = args - self._finally = try_finally - - @staticmethod - def _lazy_compile(func): - """Compile the source of a wrapped function - - Assemble and compile the decorated function, and intrusively replace its - code with the compiled version's. The thinly wrapped function becomes - the decorated function. - - Parameters - ---------- - func : callable - A function returned by argmap.__call__ which is in the process - of being called for the first time. - - Returns - ------- - func : callable - The same function, with a new __code__ object. - - Notes - ----- - It was observed in easygraph issue #4732 [1] that the import time of - easygraph was significantly bloated by the use of decorators: over half - of the import time was being spent decorating functions. This was - somewhat improved by a change made to the `decorator` library, at the - cost of a relatively heavy-weight call to `inspect.Signature.bind` - for each call to the decorated function. - - The workaround we arrived at is to do minimal work at the time of - decoration. When the decorated function is called for the first time, - we compile a function with the same function signature as the wrapped - function. The resulting decorated function is faster than one made by - the `decorator` library, so that the overhead of the first call is - 'paid off' after a small number of calls. - """ - real_func = func.__argmap__.compile(func.__wrapped__) - func.__code__ = real_func.__code__ - func.__globals__.update(real_func.__globals__) - func.__dict__.update(real_func.__dict__) - return func - - def __call__(self, f): - """Construct a lazily decorated wrapper of f. - - The decorated function will be compiled when it is called for the first time, - and it will replace its own __code__ object so subsequent calls are fast. - - Parameters - ---------- - f : callable - A function to be decorated. - - Returns - ------- - func : callable - The decorated function. - - See Also - -------- - argmap._lazy_compile - """ - - if inspect.isgeneratorfunction(f): - - def func(*args, __wrapper=None, **kwargs): - yield from argmap._lazy_compile(__wrapper)(*args, **kwargs) - - else: - - def func(*args, __wrapper=None, **kwargs): - return argmap._lazy_compile(__wrapper)(*args, **kwargs) - - # standard function-wrapping stuff - func.__name__ = f.__name__ - func.__doc__ = f.__doc__ - func.__defaults__ = f.__defaults__ - func.__kwdefaults__.update(f.__kwdefaults__ or {}) - func.__module__ = f.__module__ - func.__qualname__ = f.__qualname__ - func.__dict__.update(f.__dict__) - func.__wrapped__ = f - - # now that we've wrapped f, we may have picked up some __dict__ or - # __kwdefaults__ items that were set by a previous argmap. Thus, we set - # these values after those update() calls. - - # If we attempt to access func from within itself, that happens through - # a closure -- which trips an error when we replace func.__code__. The - # standard workaround for functions which can't see themselves is to use - # a Y-combinator, as we do here. - func.__kwdefaults__["_argmap__wrapper"] = func - - # this self-reference is here because functools.wraps preserves - # everything in __dict__, and we don't want to mistake a non-argmap - # wrapper for an argmap wrapper - func.__self__ = func - - # this is used to variously call self.assemble and self.compile - func.__argmap__ = self - - return func - - __count = 0 - - @classmethod - def _count(cls): - """Maintain a globally-unique identifier for function names and "file" names - - Note that this counter is a class method reporting a class variable - so the count is unique within a Python session. It could differ from - session to session for a specific decorator depending on the order - that the decorators are created. But that doesn't disrupt `argmap`. - - This is used in two places: to construct unique variable names - in the `_name` method and to construct unique fictitious filenames - in the `_compile` method. - - Returns - ------- - count : int - An integer unique to this Python session (simply counts from zero) - """ - cls.__count += 1 - return cls.__count - - _bad_chars = re.compile("[^a-zA-Z0-9_]") - - @classmethod - def _name(cls, f): - """Mangle the name of a function to be unique but somewhat human-readable - - The names are unique within a Python session and set using `_count`. - - Parameters - ---------- - f : str or object - - Returns - ------- - name : str - The mangled version of `f.__name__` (if `f.__name__` exists) or `f` - - """ - f = f.__name__ if hasattr(f, "__name__") else f - fname = re.sub(cls._bad_chars, "_", f) - return f"argmap_{fname}_{cls._count()}" - - def compile(self, f): - """Compile the decorated function. - - Called once for a given decorated function -- collects the code from all - argmap decorators in the stack, and compiles the decorated function. - - Much of the work done here uses the `assemble` method to allow recursive - treatment of multiple argmap decorators on a single decorated function. - That flattens the argmap decorators, collects the source code to construct - a single decorated function, then compiles/executes/returns that function. - - The source code for the decorated function is stored as an attribute - `_code` on the function object itself. - - Note that Python's `compile` function requires a filename, but this - code is constructed without a file, so a fictitious filename is used - to describe where the function comes from. The name is something like: - "argmap compilation 4". - - Parameters - ---------- - f : callable - The function to be decorated - - Returns - ------- - func : callable - The decorated file - - """ - sig, wrapped_name, functions, mapblock, finallys, mutable_args = self.assemble( - f - ) - - call = f"{sig.call_sig.format(wrapped_name)}#" - mut_args = f"{sig.args} = list({sig.args})" if mutable_args else "" - body = argmap._indent(sig.def_sig, mut_args, mapblock, call, finallys) - code = "\n".join(body) - - locl = {} - globl = dict(functions.values()) - filename = f"{self.__class__} compilation {self._count()}" - compiled = compile(code, filename, "exec") - exec(compiled, globl, locl) - func = locl[sig.name] - func._code = code - return func - - def assemble(self, f): - """Collects components of the source for the decorated function wrapping f. - - If `f` has multiple argmap decorators, we recursively assemble the stack of - decorators into a single flattened function. - - This method is part of the `compile` method's process yet separated - from that method to allow recursive processing. The outputs are - strings, dictionaries and lists that collect needed info to - flatten any nested argmap-decoration. - - Parameters - ---------- - f : callable - The function to be decorated. If f is argmapped, we assemble it. - - Returns - ------- - sig : argmap.Signature - The function signature as an `argmap.Signature` object. - wrapped_name : str - The mangled name used to represent the wrapped function in the code - being assembled. - functions : dict - A dictionary mapping id(g) -> (mangled_name(g), g) for functions g - referred to in the code being assembled. These need to be present - in the ``globals`` scope of ``exec`` when defining the decorated - function. - mapblock : list of lists and/or strings - Code that implements mapping of parameters including any try blocks - if needed. This code will precede the decorated function call. - finallys : list of lists and/or strings - Code that implements the finally blocks to post-process the - arguments (usually close any files if needed) after the - decorated function is called. - mutable_args : bool - True if the decorator needs to modify positional arguments - via their indices. The compile method then turns the argument - tuple into a list so that the arguments can be modified. - """ - - # first, we check if f is already argmapped -- if that's the case, - # build up the function recursively. - # > mapblock is generally a list of function calls of the sort - # arg = func(arg) - # in addition to some try-blocks if needed. - # > finallys is a recursive list of finally blocks of the sort - # finally: - # close_func_1() - # finally: - # close_func_2() - # > functions is a dict of functions used in the scope of our decorated - # function. It will be used to construct globals used in compilation. - # We make functions[id(f)] = name_of_f, f to ensure that a given - # function is stored and named exactly once even if called by - # nested decorators. - if hasattr(f, "__argmap__") and f.__self__ is f: - ( - sig, - wrapped_name, - functions, - mapblock, - finallys, - mutable_args, - ) = f.__argmap__.assemble(f.__wrapped__) - functions = dict(functions) # shallow-copy just in case - else: - sig = self.signature(f) - wrapped_name = self._name(f) - mapblock, finallys = [], [] - functions = {id(f): (wrapped_name, f)} - mutable_args = False - - if id(self._func) in functions: - fname, _ = functions[id(self._func)] - else: - fname, _ = functions[id(self._func)] = self._name(self._func), self._func - - # this is a bit complicated -- we can call functions with a variety of - # nested arguments, so long as their input and output are tuples with - # the same nested structure. e.g. ("a", "b") maps arguments a and b. - # A more complicated nesting like (0, (3, 4)) maps arguments 0, 3, 4 - # expecting the mapping to output new values in the same nested shape. - # while we're not taking full advantage of the ability to handle - # multiply-nested tuples, it was convenient to implement this in - # generality because the recursive call to `get_name` is necessary in - # any case. - applied = set() - - def get_name(arg, first=True): - nonlocal mutable_args - if isinstance(arg, tuple): - name = ", ".join(get_name(x, False) for x in arg) - return name if first else f"({name})" - if arg in applied: - raise EasyGraphError(f"argument {arg} is specified multiple times") - applied.add(arg) - if arg in sig.names: - return sig.names[arg] - elif isinstance(arg, str): - if sig.kwargs is None: - raise EasyGraphError( - f"name {arg} is not a named parameter and this function doesn't" - " have kwargs" - ) - return f"{sig.kwargs}[{arg!r}]" - else: - if sig.args is None: - raise EasyGraphError( - f"index {arg} not a parameter index and this function doesn't" - " have args" - ) - mutable_args = True - return f"{sig.args}[{arg - sig.n_positional}]" - - if self._finally: - # here's where we handle try_finally decorators. Such a decorator - # returns a mapped argument and a function to be called in a - # finally block. This feature was required by the open_file - # decorator. The below generates the code - # - # name, final = func(name) #<--append to mapblock - # try: #<--append to mapblock - # ... more argmapping and try blocks - # return WRAPPED_FUNCTION(...) - # ... more finally blocks - # finally: #<--prepend to finallys - # final() #<--prepend to finallys - # - for a in self._args: - name = get_name(a) - final = self._name(name) - mapblock.append(f"{name}, {final} = {fname}({name})") - mapblock.append("try:") - finallys = ["finally:", f"{final}()#", "#", finallys] - else: - mapblock.extend( - f"{name} = {fname}({name})" for name in map(get_name, self._args) - ) - - return sig, wrapped_name, functions, mapblock, finallys, mutable_args - - @classmethod - def signature(cls, f): - r"""Construct a Signature object describing `f` - - Compute a Signature so that we can write a function wrapping f with - the same signature and call-type. - - Parameters - ---------- - f : callable - A function to be decorated - - Returns - ------- - sig : argmap.Signature - The Signature of f - - Notes - ----- - The Signature is a namedtuple with names: - - name : a unique version of the name of the decorated function - signature : the inspect.signature of the decorated function - def_sig : a string used as code to define the new function - call_sig : a string used as code to call the decorated function - names : a dict keyed by argument name and index to the argument's name - n_positional : the number of positional arguments in the signature - args : the name of the VAR_POSITIONAL argument if any, i.e. \*theseargs - kwargs : the name of the VAR_KEYWORDS argument if any, i.e. \*\*kwargs - - These named attributes of the signature are used in `assemble` and `compile` - to construct a string of source code for the decorated function. - - """ - sig = inspect.signature(f, follow_wrapped=False) - def_sig = [] - call_sig = [] - names = {} - - kind = None - args = None - kwargs = None - npos = 0 - for i, param in enumerate(sig.parameters.values()): - # parameters can be position-only, keyword-or-position, keyword-only - # in any combination, but only in the order as above. we do edge - # detection to add the appropriate punctuation - prev = kind - kind = param.kind - if prev == param.POSITIONAL_ONLY != kind: - # the last token was position-only, but this one isn't - def_sig.append("/") - if prev != param.KEYWORD_ONLY == kind != param.VAR_POSITIONAL: - # param is the first keyword-only arg and isn't starred - def_sig.append("*") - - # star arguments as appropriate - if kind == param.VAR_POSITIONAL: - name = "*" + param.name - args = param.name - count = 0 - elif kind == param.VAR_KEYWORD: - name = "**" + param.name - kwargs = param.name - count = 0 - else: - names[i] = names[param.name] = param.name - name = param.name - count = 1 - - # assign to keyword-only args in the function call - if kind == param.KEYWORD_ONLY: - call_sig.append(f"{name} = {name}") - else: - npos += count - call_sig.append(name) - - def_sig.append(name) - - fname = cls._name(f) - def_sig = f'def {fname}({", ".join(def_sig)}):' - - if inspect.isgeneratorfunction(f): - _return = "yield from" - else: - _return = "return" - - call_sig = f"{_return} {{}}({', '.join(call_sig)})" - - return cls.Signature(fname, sig, def_sig, call_sig, names, npos, args, kwargs) - - Signature = collections.namedtuple( - "Signature", - [ - "name", - "signature", - "def_sig", - "call_sig", - "names", - "n_positional", - "args", - "kwargs", - ], - ) - - @staticmethod - def _flatten(nestlist, visited): - """flattens a recursive list of lists that doesn't have cyclic references - - Parameters - ---------- - nestlist : iterable - A recursive list of objects to be flattened into a single iterable - - visited : set - A set of object ids which have been walked -- initialize with an - empty set - - Yields - ------ - Non-list objects contained in nestlist - - """ - for thing in nestlist: - if isinstance(thing, list): - if id(thing) in visited: - raise ValueError("A cycle was found in nestlist. Be a tree.") - else: - visited.add(id(thing)) - yield from argmap._flatten(thing, visited) - else: - yield thing - - _tabs = " " * 64 - - @staticmethod - def _indent(*lines): - """Indent list of code lines to make executable Python code - - Indents a tree-recursive list of strings, following the rule that one - space is added to the tab after a line that ends in a colon, and one is - removed after a line that ends in an hashmark. - - Parameters - ---------- - *lines : lists and/or strings - A recursive list of strings to be assembled into properly indented - code. - - Returns - ------- - code : str - - Examples - -------- - - argmap._indent(*["try:", "try:", "pass#", "finally:", "pass#", "#", - "finally:", "pass#"]) - - renders to - - '''try: - try: - pass# - finally: - pass# - # - finally: - pass#''' - """ - depth = 0 - for line in argmap._flatten(lines, set()): - yield f"{argmap._tabs[:depth]}{line}" - depth += (line[-1:] == ":") - (line[-1:] == "#") - - -
[docs]def nodes_or_number(which_args): - """Decorator to allow number of nodes or container of nodes. - - With this decorator, the specified argument can be either a number or a container - of nodes. If it is a number, the nodes used are `range(n)`. - This allows `eg.complete_graph(50)` in place of `eg.complete_graph(list(range(50)))`. - And it also allows `eg.complete_graph(any_list_of_nodes)`. - - Parameters - ---------- - which_args : string or int or sequence of strings or ints - If string, the name of the argument to be treated. - If int, the index of the argument to be treated. - If more than one node argument is allowed, can be a list of locations. - - Returns - ------- - _nodes_or_numbers : function - Function which replaces int args with ranges. - - Examples - -------- - Decorate functions like this:: - - @nodes_or_number("nodes") - def empty_graph(nodes): - # nodes is converted to a list of nodes - - @nodes_or_number(0) - def empty_graph(nodes): - # nodes is converted to a list of nodes - - @nodes_or_number(["m1", "m2"]) - def grid_2d_graph(m1, m2, periodic=False): - # m1 and m2 are each converted to a list of nodes - - @nodes_or_number([0, 1]) - def grid_2d_graph(m1, m2, periodic=False): - # m1 and m2 are each converted to a list of nodes - - @nodes_or_number(1) - def full_rary_tree(r, n) - # presumably r is a number. It is not handled by this decorator. - # n is converted to a list of nodes - """ - - def _nodes_or_number(n): - try: - nodes = list(range(n)) - except TypeError: - nodes = tuple(n) - else: - if n < 0: - msg = "Negative number of nodes not valid: {n}" - raise EasyGraphError(msg) - return (n, nodes) - - try: - iter_wa = iter(which_args) - except TypeError: - iter_wa = (which_args,) - - return argmap(_nodes_or_number, *iter_wa)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/download.html b/docs/_modules/easygraph/utils/download.html deleted file mode 100644 index 33e17cd0..00000000 --- a/docs/_modules/easygraph/utils/download.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - easygraph.utils.download — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.download

-import hashlib
-import warnings
-
-from functools import wraps
-from pathlib import Path
-
-import requests
-
-
-__all__ = [
-    "check_file",
-    "download_file",
-    "download_and_check",
-]
-
-
-
[docs]def download_file(url: str, file_path: Path): - r"""Download a file from a url. - - Args: - ``url`` (``str``): the url of the file - ``file_path`` (``str``): the path to the file - """ - file_path.parent.mkdir(parents=True, exist_ok=True) - r = requests.get(url, stream=True, verify=True) - if r.status_code != 200: - raise requests.HTTPError(f"{url} is not accessible.") - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=1024): - if chunk: - f.write(chunk)
- - -
[docs]def check_file(file_path: Path, md5: str): - r"""Check if a file is valid. - - Args: - ``file_path`` (``Path``): The local path of the file. - ``md5`` (``str``): The md5 of the file. - - Raises: - FileNotFoundError: Not found the file. - """ - if not file_path.exists(): - raise FileNotFoundError(f"{file_path} does not exist.") - else: - with open(file_path, "rb") as f: - data = f.read() - cur_md5 = hashlib.md5(data).hexdigest() - return cur_md5 == md5
- - -def _retry(n: int, exception_type=requests.HTTPError): - r"""A decorator for retrying a function for n times. - - Args: - ``n`` (``int``): The number of times to retry. - """ - - def decorator(fetcher): - @wraps(fetcher) - def wrapper(*args, **kwargs): - for i in range(n - 1): - try: - return fetcher(*args, **kwargs) - except exception_type as e: - warnings.warn(f"Retry downloading({i + 1}/{n}): {str(e)}") - except Exception as e: - raise e - return fetcher(*args, **kwargs) - # raise FileNotFoundError - - return wrapper - - return decorator - - -
[docs]@_retry(3) -def download_and_check(url: str, file_path: Path, md5: str): - r"""Download a file from a url and check its integrity. - - Args: - ``url`` (``str``): The url of the file. - ``file_path`` (``Path``): The path to the file. - ``md5`` (``str``): The md5 of the file. - """ - if not file_path.exists(): - download_file(url, file_path) - if not check_file(file_path, md5): - file_path.unlink() - raise ValueError( - f"{file_path} is corrupted. We will delete it, and try to download it" - " again." - ) - return True
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/exception.html b/docs/_modules/easygraph/utils/exception.html deleted file mode 100644 index 15a0c2d9..00000000 --- a/docs/_modules/easygraph/utils/exception.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - easygraph.utils.exception — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • »
  • -
  • Module code »
  • -
  • easygraph.utils.exception
  • -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.utils.exception

-"""
-**********
-Exceptions
-**********
-
-Base exceptions and errors for EasyGraph.
-"""
-
-
-__all__ = [
-    "EasyGraphException",
-    "EasyGraphError",
-    "EasyGraphNotImplemented",
-    "EasyGraphPointlessConcept",
-]
-
-
-
[docs]class EasyGraphException(Exception): - """Base class for exceptions in EasyGraph."""
- - -
[docs]class EasyGraphError(EasyGraphException): - """Exception for a serious error in EasyGraph"""
- - -
[docs]class EasyGraphNotImplemented(EasyGraphException): - """Exception raised by algorithms not implemented for a type of graph."""
- - -
[docs]class EasyGraphPointlessConcept(EasyGraphException): - """Raised when a null graph is provided as input to an algorithm - that cannot use it. - - The null graph is sometimes considered a pointless concept [1]_, - thus the name of the exception. - - References - ---------- - .. [1] Harary, F. and Read, R. "Is the Null Graph a Pointless - Concept?" In Graphs and Combinatorics Conference, George - Washington University. New York: Springer-Verlag, 1973. - - """
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/index_of_node.html b/docs/_modules/easygraph/utils/index_of_node.html deleted file mode 100644 index bae3d3d6..00000000 --- a/docs/_modules/easygraph/utils/index_of_node.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - easygraph.utils.index_of_node — EasyGraph 1.4 - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • »
  • -
  • Module code »
  • -
  • easygraph.utils.index_of_node
  • -
  • -
  • -
-
-
-
-
- -

Source code for easygraph.utils.index_of_node

-__all__ = ["get_relation_of_index_and_node"]
-
-
-
[docs]def get_relation_of_index_and_node(graph): - node2idx = {} - idx2node = [] - node_size = 0 - for node in graph.nodes: - node2idx[node] = node_size - idx2node.append(node) - node_size += 1 - return idx2node, node2idx
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/logging.html b/docs/_modules/easygraph/utils/logging.html deleted file mode 100644 index 8d0f5131..00000000 --- a/docs/_modules/easygraph/utils/logging.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - easygraph.utils.logging — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.logging

-import logging
-import sys
-
-from pathlib import Path
-from typing import Union
-
-
-__all__ = ["default_log_formatter", "simple_stdout2file"]
-
-
-
[docs]def default_log_formatter() -> logging.Formatter: - r"""Create a default formatter of log messages for logging.""" - - return logging.Formatter("[%(levelname)s %(asctime)s]-> %(message)s")
- - -
[docs]def simple_stdout2file(file_path: Union[str, Path]) -> None: - r"""This function simply wraps the ``sys.stdout`` stream, and outputs messages to the ``sys.stdout`` and a specified file, simultaneously. - - Parameters: - ``file_path`` (``file_path: Union[str, Path]``): The path of the file to output the messages. - """ - - class SimpleLogger: - def __init__(self, file_path: Path): - file_path = Path(file_path).absolute() - assert ( - file_path.parent.exists() - ), f"The parent directory of {file_path} does not exist." - self.file_path = file_path - self.terminal = sys.stdout - self.file = open(file_path, "a") - - def write(self, message): - self.terminal.write(message) - self.file.write(message) - self.flush() - - def flush(self): - self.terminal.flush() - self.file.flush() - - file_path = Path(file_path) - sys.stdout = SimpleLogger(file_path)
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/mapped_queue.html b/docs/_modules/easygraph/utils/mapped_queue.html deleted file mode 100644 index f6ea7fb2..00000000 --- a/docs/_modules/easygraph/utils/mapped_queue.html +++ /dev/null @@ -1,299 +0,0 @@ - - - - - - easygraph.utils.mapped_queue — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.mapped_queue

-"""
-Priority queue class with updatable priorities.
-Codes from NetworkX - http://networkx.github.io/
-"""
-
-
-import heapq
-
-
-__all__ = ["MappedQueue"]
-
-
-
[docs]class MappedQueue: - """ - The MappedQueue class implements an efficient minimum heap. The - smallest element can be popped in O(1) time, new elements can be pushed - in O(log n) time, and any element can be removed or updated in O(log n) - time. The queue cannot contain duplicate elements and an attempt to push an - element already in the queue will have no effect. - - MappedQueue complements the heapq package from the python standard - library. While MappedQueue is designed for maximum compatibility with - heapq, it has slightly different functionality. - - Examples - -------- - - A `MappedQueue` can be created empty or optionally given an array of - initial elements. Calling `push()` will add an element and calling `pop()` - will remove and return the smallest element. - - >>> q = MappedQueue([916, 50, 4609, 493, 237]) - >>> q.push(1310) - True - >>> x = [q.pop() for i in range(len(q.h))] - >>> x - [50, 237, 493, 916, 1310, 4609] - - Elements can also be updated or removed from anywhere in the queue. - - >>> q = MappedQueue([916, 50, 4609, 493, 237]) - >>> q.remove(493) - >>> q.update(237, 1117) - >>> x = [q.pop() for i in range(len(q.h))] - >>> x - [50, 916, 1117, 4609] - - References - ---------- - .. [1] Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2001). - Introduction to algorithms second edition. - .. [2] Knuth, D. E. (1997). The art of computer programming (Vol. 3). - Pearson Education. - """ - - def __init__(self, data=[]): - """Priority queue class with updatable priorities.""" - self.h = list(data) - self.d = dict() - self._heapify() - - def __len__(self): - return len(self.h) - - def _heapify(self): - """Restore heap invariant and recalculate map.""" - heapq.heapify(self.h) - self.d = {elt: pos for pos, elt in enumerate(self.h)} - if len(self.h) != len(self.d): - raise AssertionError("Heap contains duplicate elements") - -
[docs] def push(self, elt): - """Add an element to the queue.""" - # If element is already in queue, do nothing - if elt in self.d: - return False - # Add element to heap and dict - pos = len(self.h) - self.h.append(elt) - self.d[elt] = pos - # Restore invariant by sifting down - self._siftdown(pos) - return True
- -
[docs] def pop(self): - """Remove and return the smallest element in the queue.""" - # Remove smallest element - elt = self.h[0] - del self.d[elt] - # If elt is last item, remove and return - if len(self.h) == 1: - self.h.pop() - return elt - # Replace root with last element - last = self.h.pop() - self.h[0] = last - self.d[last] = 0 - # Restore invariant by sifting up, then down - pos = self._siftup(0) - self._siftdown(pos) - # Return smallest element - return elt
- -
[docs] def update(self, elt, new): - """Replace an element in the queue with a new one.""" - # Replace - pos = self.d[elt] - self.h[pos] = new - del self.d[elt] - self.d[new] = pos - # Restore invariant by sifting up, then down - pos = self._siftup(pos) - self._siftdown(pos)
- -
[docs] def remove(self, elt): - """Remove an element from the queue.""" - # Find and remove element - try: - pos = self.d[elt] - del self.d[elt] - except KeyError: - # Not in queue - raise - # If elt is last item, remove and return - if pos == len(self.h) - 1: - self.h.pop() - return - # Replace elt with last element - last = self.h.pop() - self.h[pos] = last - self.d[last] = pos - # Restore invariant by sifting up, then down - pos = self._siftup(pos) - self._siftdown(pos)
- - def _siftup(self, pos): - """Move element at pos down to a leaf by repeatedly moving the smaller - child up.""" - h, d = self.h, self.d - elt = h[pos] - # Continue until element is in a leaf - end_pos = len(h) - left_pos = (pos << 1) + 1 - while left_pos < end_pos: - # Left child is guaranteed to exist by loop predicate - left = h[left_pos] - try: - right_pos = left_pos + 1 - right = h[right_pos] - # Out-of-place, swap with left unless right is smaller - if right < left: - h[pos], h[right_pos] = right, elt - pos, right_pos = right_pos, pos - d[elt], d[right] = pos, right_pos - else: - h[pos], h[left_pos] = left, elt - pos, left_pos = left_pos, pos - d[elt], d[left] = pos, left_pos - except IndexError: - # Left leaf is the end of the heap, swap - h[pos], h[left_pos] = left, elt - pos, left_pos = left_pos, pos - d[elt], d[left] = pos, left_pos - # Update left_pos - left_pos = (pos << 1) + 1 - return pos - - def _siftdown(self, pos): - """Restore invariant by repeatedly replacing out-of-place element with - its parent.""" - h, d = self.h, self.d - elt = h[pos] - # Continue until element is at root - while pos > 0: - parent_pos = (pos - 1) >> 1 - parent = h[parent_pos] - if parent > elt: - # Swap out-of-place element with parent - h[parent_pos], h[pos] = elt, parent - parent_pos, pos = pos, parent_pos - d[elt] = pos - d[parent] = parent_pos - else: - # Invariant is satisfied - break - return pos
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/misc.html b/docs/_modules/easygraph/utils/misc.html deleted file mode 100644 index 963a670b..00000000 --- a/docs/_modules/easygraph/utils/misc.html +++ /dev/null @@ -1,335 +0,0 @@ - - - - - - easygraph.utils.misc — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.misc

-from collections.abc import Iterable
-from collections.abc import Iterator
-from itertools import chain
-from itertools import tee
-
-
-__all__ = [
-    "split_len",
-    "split",
-    "nodes_equal",
-    "edges_equal",
-    "pairwise",
-    "graphs_equal",
-    # "arbitrary_element"
-]
-
-
-
[docs]def split_len(nodes, step=30000): - ret = [] - length = len(nodes) - for i in range(0, length, step): - ret.append(nodes[i : i + step]) - if len(ret[-1]) * 3 < step: - ret[-2] = ret[-2] + ret[-1] - ret = ret[:-1] - return ret
- - -
[docs]def split(nodes, n): - ret = [] - length = len(nodes) # 总长 - step = int(length / n) + 1 # 每份的长度 - for i in range(0, length, step): - ret.append(nodes[i : i + step]) - return ret
- - -
[docs]def nodes_equal(nodes1, nodes2): - """Check if nodes are equal. - - Equality here means equal as Python objects. - Node data must match if included. - The order of nodes is not relevant. - - Parameters - ---------- - nodes1, nodes2 : iterables of nodes, or (node, datadict) tuples - - Returns - ------- - bool - True if nodes are equal, False otherwise. - """ - nlist1 = list(nodes1) - nlist2 = list(nodes2) - try: - d1 = dict(nlist1) - d2 = dict(nlist2) - except (ValueError, TypeError): - d1 = dict.fromkeys(nlist1) - d2 = dict.fromkeys(nlist2) - return d1 == d2
- - -
[docs]def edges_equal(edges1, edges2, need_data=True): - """Check if edges are equal. - - Equality here means equal as Python objects. - Edge data must match if included. - The order of the edges is not relevant. - - Parameters - ---------- - edges1, edges2 : iterables of with u, v nodes as - edge tuples (u, v), or - edge tuples with data dicts (u, v, d), or - edge tuples with keys and data dicts (u, v, k, d) - - Returns - ------- - bool - True if edges are equal, False otherwise. - """ - from collections import defaultdict - - d1 = defaultdict(dict) - d2 = defaultdict(dict) - c1 = 0 - for c1, e in enumerate(edges1): - u, v = e[0], e[1] - data = [] - if need_data == True: - data = [e[2:]] - if v in d1[u]: - data = d1[u][v] + data - d1[u][v] = data - d1[v][u] = data - c2 = 0 - for c2, e in enumerate(edges2): - u, v = e[0], e[1] - data = [] - if need_data == True: - data = [e[2:]] - if v in d2[u]: - data = d2[u][v] + data - d2[u][v] = data - d2[v][u] = data - if c1 != c2: - return False - # can check one direction because lengths are the same. - for n, nbrdict in d1.items(): - for nbr, datalist in nbrdict.items(): - if n not in d2: - return False - if nbr not in d2[n]: - return False - d2datalist = d2[n][nbr] - for data in datalist: - if datalist.count(data) != d2datalist.count(data): - return False - return True
- - -# Recipe from the itertools documentation. -
[docs]def pairwise(iterable, cyclic=False): - "s -> (s0, s1), (s1, s2), (s2, s3), ..." - a, b = tee(iterable) - first = next(b, None) - if cyclic is True: - return zip(a, chain(b, (first,))) - return zip(a, b)
- - -
[docs]def graphs_equal(graph1, graph2): - """Check if graphs are equal. - - Equality here means equal as Python objects (not isomorphism). - Node, edge and graph data must match. - - Parameters - ---------- - graph1, graph2 : graph - - Returns - ------- - bool - True if graphs are equal, False otherwise. - """ - return ( - graph1.adj == graph2.adj - and graph1.nodes == graph2.nodes - and graph1.graph == graph2.graph - )
- - -# def arbitrary_element(iterable): -# """Returns an arbitrary element of `iterable` without removing it. - -# This is most useful for "peeking" at an arbitrary element of a set, -# but can be used for any list, dictionary, etc., as well. - -# Parameters -# ---------- -# iterable : `abc.collections.Iterable` instance -# Any object that implements ``__iter__``, e.g. set, dict, list, tuple, -# etc. - -# Returns -# ------- -# The object that results from ``next(iter(iterable))`` - -# Raises -# ------ -# ValueError -# If `iterable` is an iterator (because the current implementation of -# this function would consume an element from the iterator). - -# Examples -# -------- -# Arbitrary elements from common Iterable objects: - -# >>> eg.utils.arbitrary_element([1, 2, 3]) # list -# 1 -# >>> eg.utils.arbitrary_element((1, 2, 3)) # tuple -# 1 -# >>> eg.utils.arbitrary_element({1, 2, 3}) # set -# 1 -# >>> d = {k: v for k, v in zip([1, 2, 3], [3, 2, 1])} -# >>> eg.utils.arbitrary_element(d) # dict_keys -# 1 -# >>> eg.utils.arbitrary_element(d.values()) # dict values -# 3 - -# `str` is also an Iterable: - -# >>> eg.utils.arbitrary_element("hello") -# 'h' - -# :exc:`ValueError` is raised if `iterable` is an iterator: - -# >>> iterator = iter([1, 2, 3]) # Iterator, *not* Iterable -# >>> eg.utils.arbitrary_element(iterator) -# Traceback (most recent call last): -# ... -# ValueError: cannot return an arbitrary item from an iterator - -# Notes -# ----- -# This function does not return a *random* element. If `iterable` is -# ordered, sequential calls will return the same value:: - -# >>> l = [1, 2, 3] -# >>> eg.utils.arbitrary_element(l) -# 1 -# >>> eg.utils.arbitrary_element(l) -# 1 - -# """ -# if isinstance(iterable, Iterator): -# raise ValueError("cannot return an arbitrary item from an iterator") -# # Another possible implementation is ``for x in iterable: return x``. -# return next(iter(iterable)) -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/relabel.html b/docs/_modules/easygraph/utils/relabel.html deleted file mode 100644 index b1113ece..00000000 --- a/docs/_modules/easygraph/utils/relabel.html +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - easygraph.utils.relabel — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.relabel

-import easygraph as eg
-
-
-__all__ = ["relabel_nodes", "convert_node_labels_to_integers"]
-
-
-
[docs]def relabel_nodes(G, mapping): - if not hasattr(mapping, "__getitem__"): - m = {n: mapping(n) for n in G} - else: - m = mapping - return _relabel_copy(G, m)
- - -def _relabel_copy(G, mapping): - H = G.__class__() - H.add_nodes_from(mapping.get(n, n) for n in G) - H._node.update((mapping.get(n, n), d.copy()) for n, d in G.nodes.items()) - if G.is_multigraph(): - new_edges = [ - (mapping.get(n1, n1), mapping.get(n2, n2), k, d.copy()) - for (n1, n2, k, d) in G.edges - ] - - # check for conflicting edge-keys - undirected = not G.is_directed() - seen_edges = set() - for i, (source, target, key, data) in enumerate(new_edges): - while (source, target, key) in seen_edges: - if not isinstance(key, (int, float)): - key = 0 - key += 1 - seen_edges.add((source, target, key)) - if undirected: - seen_edges.add((target, source, key)) - new_edges[i] = (source, target, key, data) - - H.add_edges_from(new_edges) - else: - H.add_edges_from( - (mapping.get(n1, n1), mapping.get(n2, n2), d.copy()) - for (n1, n2, d) in G.edges - ) - H.graph.update(G.graph) - return H - - -
[docs]def convert_node_labels_to_integers( - G, first_label=0, ordering="default", label_attribute=None -): - """Returns a copy of the graph G with the nodes relabeled using - consecutive integers. - - Parameters - ---------- - G : graph - A easygraph graph - - first_label : int, optional (default=0) - An integer specifying the starting offset in numbering nodes. - The new integer labels are numbered first_label, ..., n-1+first_label. - - ordering : string - "default" : inherit node ordering from G.nodes - "sorted" : inherit node ordering from sorted(G.nodes) - "increasing degree" : nodes are sorted by increasing degree - "decreasing degree" : nodes are sorted by decreasing degree - - label_attribute : string, optional (default=None) - Name of node attribute to store old label. If None no attribute - is created. - - Notes - ----- - Node and edge attribute data are copied to the new (relabeled) graph. - - There is no guarantee that the relabeling of nodes to integers will - give the same two integers for two (even identical graphs). - Use the `ordering` argument to try to preserve the order. - - See Also - -------- - relabel_nodes - """ - N = G.number_of_nodes() + first_label - if ordering == "default": - mapping = dict(zip(G.nodes, range(first_label, N))) - elif ordering == "sorted": - nlist = sorted(G.nodes) - mapping = dict(zip(nlist, range(first_label, N))) - elif ordering == "increasing degree": - dv_pairs = [(d, n) for (n, d) in G.degree()] - dv_pairs.sort() # in-place sort from lowest to highest degree - mapping = dict(zip([n for d, n in dv_pairs], range(first_label, N))) - elif ordering == "decreasing degree": - dv_pairs = [(d, n) for (n, d) in G.degree()] - dv_pairs.sort() # in-place sort from lowest to highest degree - dv_pairs.reverse() - mapping = dict(zip([n for d, n in dv_pairs], range(first_label, N))) - else: - raise eg.EasyGraphError(f"Unknown node ordering: {ordering}") - H = relabel_nodes(G, mapping) - # create node attribute with the old label - if label_attribute is not None: - eg.set_node_attributes(H, {v: k for k, v in mapping.items()}, label_attribute) - return H
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/sparse.html b/docs/_modules/easygraph/utils/sparse.html deleted file mode 100644 index 3af55e7d..00000000 --- a/docs/_modules/easygraph/utils/sparse.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - easygraph.utils.sparse — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.sparse

-__all__ = ["sparse_dropout"]
-
-
-# if not type checking
-from typing import TYPE_CHECKING
-
-
-if TYPE_CHECKING:
-    import torch
-
-
-
[docs]def sparse_dropout( - sp_mat: "torch.Tensor", p: float, fill_value: float = 0.0 -) -> "torch.Tensor": - import torch - - r"""Dropout function for sparse matrix. This function will return a new sparse matrix with the same shape as the input sparse matrix, but with some elements dropped out. - - Args: - ``sp_mat`` (``torch.Tensor``): The sparse matrix with format ``torch.sparse_coo_tensor``. - ``p`` (``float``): Probability of an element to be dropped. - ``fill_value`` (``float``): The fill value for dropped elements. Defaults to ``0.0``. - """ - device = sp_mat.device - sp_mat = sp_mat.coalesce() - assert 0 <= p <= 1 - if p == 0: - return sp_mat - p = torch.ones(sp_mat._nnz(), device=device) * p - keep_mask = torch.bernoulli(1 - p).to(device) - fill_values = torch.logical_not(keep_mask) * fill_value - new_sp_mat = torch.sparse_coo_tensor( - sp_mat._indices(), - sp_mat._values() * keep_mask + fill_values, - size=sp_mat.size(), - device=sp_mat.device, - dtype=sp_mat.dtype, - ) - return new_sp_mat
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/easygraph/utils/type_change.html b/docs/_modules/easygraph/utils/type_change.html deleted file mode 100644 index b21ca97f..00000000 --- a/docs/_modules/easygraph/utils/type_change.html +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - easygraph.utils.type_change — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for easygraph.utils.type_change

-import easygraph as eg
-
-
-__all__ = [
-    "from_pyGraphviz_agraph",
-    "to_pyGraphviz_agraph",
-]
-
-
-
[docs]def from_pyGraphviz_agraph(A, create_using=None): - """Returns a EasyGraph Graph or DiGraph from a PyGraphviz graph. - - Parameters - ---------- - A : PyGraphviz AGraph - A graph created with PyGraphviz - - create_using : EasyGraph graph constructor, optional (default=None) - Graph type to create. If graph instance, then cleared before populated. - If `None`, then the appropriate Graph type is inferred from `A`. - - Examples - -------- - >>> K5 = eg.complete_graph(5) - >>> A = eg.to_pyGraphviz_agraph(K5) - >>> G = eg.from_pyGraphviz_agraph(A) - - Notes - ----- - The Graph G will have a dictionary G.graph_attr containing - the default graphviz attributes for graphs, nodes and edges. - - Default node attributes will be in the dictionary G.node_attr - which is keyed by node. - - Edge attributes will be returned as edge data in G. With - edge_attr=False the edge data will be the Graphviz edge weight - attribute or the value 1 if no edge weight attribute is found. - - """ - if create_using is None: - if A.is_directed(): - if A.is_strict(): - create_using = eg.DiGraph - else: - create_using = eg.MultiDiGraph - else: - if A.is_strict(): - create_using = eg.Graph - else: - create_using = eg.MultiGraph - - # assign defaults - N = eg.empty_graph(0, create_using) - if A.name is not None: - N.name = A.name - - # add graph attributes - N.graph.update(A.graph_attr) - - # add nodes, attributes to N.node_attr - for n in A.nodes(): - str_attr = {str(k): v for k, v in n.attr.items()} - N.add_node(str(n), **str_attr) - - # add edges, assign edge data as dictionary of attributes - for e in A.edges(): - u, v = str(e[0]), str(e[1]) - attr = dict(e.attr) - str_attr = {str(k): v for k, v in attr.items()} - if not N.is_multigraph(): - if e.name is not None: - str_attr["key"] = e.name - N.add_edge(u, v, **str_attr) - else: - N.add_edge(u, v, key=e.name, **str_attr) - - # add default attributes for graph, nodes, and edges - # hang them on N.graph_attr - N.graph["graph"] = dict(A.graph_attr) - N.graph["node"] = dict(A.node_attr) - N.graph["edge"] = dict(A.edge_attr) - return N
- - -
[docs]def to_pyGraphviz_agraph(N): - """Returns a pygraphviz graph from a EasyGraph graph N. - - Parameters - ---------- - N : EasyGraph graph - A graph created with EasyGraph - - Examples - -------- - >>> K5 = eg.complete_graph(5) - >>> A = eg.to_pyGraphviz_agraph(K5) - - Notes - ----- - If N has an dict N.graph_attr an attempt will be made first - to copy properties attached to the graph (see from_agraph) - and then updated with the calling arguments if any. - - """ - try: - import pygraphviz - except ImportError as err: - raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err - directed = N.is_directed() - strict = eg.number_of_selfloops(N) == 0 and not N.is_multigraph() - A = pygraphviz.AGraph(name=N.name, strict=strict, directed=directed) - - # default graph attributes - A.graph_attr.update(N.graph.get("graph", {})) - A.node_attr.update(N.graph.get("node", {})) - A.edge_attr.update(N.graph.get("edge", {})) - - A.graph_attr.update( - (k, v) for k, v in N.graph.items() if k not in ("graph", "node", "edge") - ) - - # add nodes - for n, nodedata in N.nodes(data=True): - A.add_node(n) - # Add node data - a = A.get_node(n) - a.attr.update({k: str(v) for k, v in nodedata.items()}) - - # loop over edges - if N.is_multigraph(): - for u, v, key, edgedata in N.edges(data=True, keys=True): - str_edgedata = {k: str(v) for k, v in edgedata.items() if k != "key"} - A.add_edge(u, v, key=str(key)) - # Add edge data - a = A.get_edge(u, v) - a.attr.update(str_edgedata) - - else: - for u, v, edgedata in N.edges(data=True): - str_edgedata = {k: str(v) for k, v in edgedata.items()} - A.add_edge(u, v) - # Add edge data - a = A.get_edge(u, v) - a.attr.update(str_edgedata) - - return A
-
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_modules/index.html b/docs/_modules/index.html deleted file mode 100644 index eec0049f..00000000 --- a/docs/_modules/index.html +++ /dev/null @@ -1,708 +0,0 @@ - - - - - - - - - - Overview: module code — EasyGraph 1.4.1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
-
-
-
-
- - - -
-
- -
- - - - - - - - - - - -
- -
- - -
-
- -
-
- -
- -
- - - - -
- -
- - -
-
- - - - - -
- -

All modules for which code is available

- - -
- - - - - -
- -
-
-
- -
- - - - -
- - -
-
-
- - - - - - - - \ No newline at end of file diff --git a/docs/_sources/eggpu.rst.txt b/docs/_sources/eggpu.rst.txt index af0cb767..b1d3a2c8 100644 --- a/docs/_sources/eggpu.rst.txt +++ b/docs/_sources/eggpu.rst.txt @@ -17,6 +17,7 @@ EGGPU is engineered with a three-layer architecture: .. image:: eggpu_architecture.png :align: center + Installation +++++++++++++ @@ -39,3 +40,54 @@ On Windows git clone --recursive https://github.com/easy-graph/Easy-Graph set EASYGRAPH_ENABLE_GPU=TRUE pip install ./Easy-Graph + +Benchmarking and Performance Evaluation ++++++++++++++ + +Our experiments were conducted on a machine equipped with a 12th Gen Intel Core i9-12900K CPU (16 cores, 24 logical processors) and an NVIDIA GeForce RTX 4090 GPU (114 SMs, 1536 max threads per SM, GPU clock speed: 2520.0 MHz, memory clock speed: 10501.0 MHz). The CUDA-based implementations demonstrated **significant acceleration**, as shown in the benchmark results below. + +.. list-table:: + :widths: 50 50 + :header-rows: 0 + + * - .. image:: res_clusterings.png + :align: center + - .. image:: res_constraint.png + :align: center + + + +Examples ++++++++++++++ + +We provide a comprehensive guide to EGGPU's GPU-accelerated functions within the **EasyGraph** ecosystem, demonstrating both native NVCC compilation of CUDA kernels and seamless invocation via our Python API. + + +.. code-block:: python + + import easygraph as eg + + def add_weighted(G): + G.add_edges( + [(0,1),(0,2),(0,3),(1,2),(1,3),(1,5),(2,3),(2,4),(3,5), + (5,6),(5,8),(6,7),(6,8),(7,8)], + edges_attr=[ + {'weight': 1},{'weight': 3},{'weight': 2},{'weight': 1}, + {'weight': 2},{'weight': 4},{'weight': 2},{'weight': 1}, + {'weight': 2},{'weight': 1},{'weight': 2},{'weight': 4}, + {'weight': 3},{'weight': 3}, + ] + ) + G_gpu = eg.GraphC() + add_weighted(G_gpu) + + print(eg.constraint(G_gpu, weight="weight")) + """ + [0.73650971 0.62929697 0.58577806 0.64878836 0.4685571 1. + 0.83901931 0.78530837 0.94929847] + """ + print(eg.effective_size(G_gpu, weight="weight")) + """ + [1.76388889 2.54166667 2.94047619 2.70833333 3.16666667 1. + 1.94791667 2.09375 1.10714286] + """ \ No newline at end of file diff --git a/docs/_static/_sphinx_javascript_frameworks_compat.js b/docs/_static/_sphinx_javascript_frameworks_compat.js deleted file mode 100644 index 81415803..00000000 --- a/docs/_static/_sphinx_javascript_frameworks_compat.js +++ /dev/null @@ -1,123 +0,0 @@ -/* Compatability shim for jQuery and underscores.js. - * - * Copyright Sphinx contributors - * Released under the two clause BSD licence - */ - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x - } - return decodeURIComponent(x.replace(/\+/g, ' ')); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} diff --git a/docs/_static/basic.css b/docs/_static/basic.css index b97662dd..7577acb1 100644 --- a/docs/_static/basic.css +++ b/docs/_static/basic.css @@ -55,7 +55,7 @@ div.sphinxsidebarwrapper { div.sphinxsidebar { float: left; - width: 270px; + width: 230px; margin-left: -100%; font-size: 90%; word-wrap: break-word; @@ -670,16 +670,6 @@ dd { margin-left: 30px; } -.sig dd { - margin-top: 0px; - margin-bottom: 0px; -} - -.sig dl { - margin-top: 0px; - margin-bottom: 0px; -} - dl > dd:last-child, dl > dd:last-child > :last-child { margin-bottom: 0; @@ -748,14 +738,6 @@ abbr, acronym { cursor: help; } -.translated { - background-color: rgba(207, 255, 207, 0.2) -} - -.untranslated { - background-color: rgba(255, 207, 207, 0.2) -} - /* -- code displays --------------------------------------------------------- */ pre { diff --git a/docs/_static/css/badge_only.css b/docs/_static/css/badge_only.css deleted file mode 100644 index 88ba55b9..00000000 --- a/docs/_static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} \ No newline at end of file diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/_static/css/fonts/Roboto-Slab-Bold.woff deleted file mode 100644 index 6cb60000..00000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Bold.woff and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 deleted file mode 100644 index 7059e231..00000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/_static/css/fonts/Roboto-Slab-Regular.woff deleted file mode 100644 index f815f63f..00000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Regular.woff and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 deleted file mode 100644 index f2c76e5b..00000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.eot b/docs/_static/css/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca9..00000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.svg b/docs/_static/css/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845e..00000000 --- a/docs/_static/css/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_static/css/fonts/fontawesome-webfont.ttf b/docs/_static/css/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2f..00000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff b/docs/_static/css/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a4..00000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff2 b/docs/_static/css/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc60..00000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff b/docs/_static/css/fonts/lato-bold-italic.woff deleted file mode 100644 index 88ad05b9..00000000 Binary files a/docs/_static/css/fonts/lato-bold-italic.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff2 b/docs/_static/css/fonts/lato-bold-italic.woff2 deleted file mode 100644 index c4e3d804..00000000 Binary files a/docs/_static/css/fonts/lato-bold-italic.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold.woff b/docs/_static/css/fonts/lato-bold.woff deleted file mode 100644 index c6dff51f..00000000 Binary files a/docs/_static/css/fonts/lato-bold.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold.woff2 b/docs/_static/css/fonts/lato-bold.woff2 deleted file mode 100644 index bb195043..00000000 Binary files a/docs/_static/css/fonts/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff b/docs/_static/css/fonts/lato-normal-italic.woff deleted file mode 100644 index 76114bc0..00000000 Binary files a/docs/_static/css/fonts/lato-normal-italic.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff2 b/docs/_static/css/fonts/lato-normal-italic.woff2 deleted file mode 100644 index 3404f37e..00000000 Binary files a/docs/_static/css/fonts/lato-normal-italic.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal.woff b/docs/_static/css/fonts/lato-normal.woff deleted file mode 100644 index ae1307ff..00000000 Binary files a/docs/_static/css/fonts/lato-normal.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal.woff2 b/docs/_static/css/fonts/lato-normal.woff2 deleted file mode 100644 index 3bf98433..00000000 Binary files a/docs/_static/css/fonts/lato-normal.woff2 and /dev/null differ diff --git a/docs/_static/css/theme.css b/docs/_static/css/theme.css deleted file mode 100644 index 0f14f106..00000000 --- a/docs/_static/css/theme.css +++ /dev/null @@ -1,4 +0,0 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/_static/debug.css b/docs/_static/debug.css deleted file mode 100644 index 74d4aec3..00000000 --- a/docs/_static/debug.css +++ /dev/null @@ -1,69 +0,0 @@ -/* - This CSS file should be overridden by the theme authors. It's - meant for debugging and developing the skeleton that this theme provides. -*/ -body { - font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji"; - background: lavender; -} -.sb-announcement { - background: rgb(131, 131, 131); -} -.sb-announcement__inner { - background: black; - color: white; -} -.sb-header { - background: lightskyblue; -} -.sb-header__inner { - background: royalblue; - color: white; -} -.sb-header-secondary { - background: lightcyan; -} -.sb-header-secondary__inner { - background: cornflowerblue; - color: white; -} -.sb-sidebar-primary { - background: lightgreen; -} -.sb-main { - background: blanchedalmond; -} -.sb-main__inner { - background: antiquewhite; -} -.sb-header-article { - background: lightsteelblue; -} -.sb-article-container { - background: snow; -} -.sb-article-main { - background: white; -} -.sb-footer-article { - background: lightpink; -} -.sb-sidebar-secondary { - background: lightgoldenrodyellow; -} -.sb-footer-content { - background: plum; -} -.sb-footer-content__inner { - background: palevioletred; -} -.sb-footer { - background: pink; -} -.sb-footer__inner { - background: salmon; -} -.sb-article { - background: white; -} diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js index 527b876c..d06a71d7 100644 --- a/docs/_static/doctools.js +++ b/docs/_static/doctools.js @@ -4,7 +4,7 @@ * * Base JavaScript utilities for all Sphinx HTML documentation. * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/docs/_static/fonts/Lato/lato-bold.eot b/docs/_static/fonts/Lato/lato-bold.eot deleted file mode 100644 index 3361183a..00000000 Binary files a/docs/_static/fonts/Lato/lato-bold.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.ttf b/docs/_static/fonts/Lato/lato-bold.ttf deleted file mode 100644 index 29f691d5..00000000 Binary files a/docs/_static/fonts/Lato/lato-bold.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.woff b/docs/_static/fonts/Lato/lato-bold.woff deleted file mode 100644 index c6dff51f..00000000 Binary files a/docs/_static/fonts/Lato/lato-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.woff2 b/docs/_static/fonts/Lato/lato-bold.woff2 deleted file mode 100644 index bb195043..00000000 Binary files a/docs/_static/fonts/Lato/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.eot b/docs/_static/fonts/Lato/lato-bolditalic.eot deleted file mode 100644 index 3d415493..00000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.ttf b/docs/_static/fonts/Lato/lato-bolditalic.ttf deleted file mode 100644 index f402040b..00000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.woff b/docs/_static/fonts/Lato/lato-bolditalic.woff deleted file mode 100644 index 88ad05b9..00000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/_static/fonts/Lato/lato-bolditalic.woff2 deleted file mode 100644 index c4e3d804..00000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.eot b/docs/_static/fonts/Lato/lato-italic.eot deleted file mode 100644 index 3f826421..00000000 Binary files a/docs/_static/fonts/Lato/lato-italic.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.ttf b/docs/_static/fonts/Lato/lato-italic.ttf deleted file mode 100644 index b4bfc9b2..00000000 Binary files a/docs/_static/fonts/Lato/lato-italic.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.woff b/docs/_static/fonts/Lato/lato-italic.woff deleted file mode 100644 index 76114bc0..00000000 Binary files a/docs/_static/fonts/Lato/lato-italic.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.woff2 b/docs/_static/fonts/Lato/lato-italic.woff2 deleted file mode 100644 index 3404f37e..00000000 Binary files a/docs/_static/fonts/Lato/lato-italic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.eot b/docs/_static/fonts/Lato/lato-regular.eot deleted file mode 100644 index 11e3f2a5..00000000 Binary files a/docs/_static/fonts/Lato/lato-regular.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.ttf b/docs/_static/fonts/Lato/lato-regular.ttf deleted file mode 100644 index 74decd9e..00000000 Binary files a/docs/_static/fonts/Lato/lato-regular.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.woff b/docs/_static/fonts/Lato/lato-regular.woff deleted file mode 100644 index ae1307ff..00000000 Binary files a/docs/_static/fonts/Lato/lato-regular.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.woff2 b/docs/_static/fonts/Lato/lato-regular.woff2 deleted file mode 100644 index 3bf98433..00000000 Binary files a/docs/_static/fonts/Lato/lato-regular.woff2 and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot deleted file mode 100644 index 79dc8efe..00000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf deleted file mode 100644 index df5d1df2..00000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff deleted file mode 100644 index 6cb60000..00000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 deleted file mode 100644 index 7059e231..00000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot deleted file mode 100644 index 2f7ca78a..00000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf deleted file mode 100644 index eb52a790..00000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff deleted file mode 100644 index f815f63f..00000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 deleted file mode 100644 index f2c76e5b..00000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 and /dev/null differ diff --git a/docs/_static/jquery.js b/docs/_static/jquery.js deleted file mode 100644 index c4c6022f..00000000 --- a/docs/_static/jquery.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.3.2 (https://getbootstrap.com/)\n * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n const instanceMap = elementMap.get(element);\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n instanceMap.set(key, instance);\n },\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n return null;\n },\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key);\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend';\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n }\n return selector;\n};\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n return prefix;\n};\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n }\n\n // Get transition-duration of the element\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n return typeof object.nodeType !== 'undefined';\n};\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object));\n }\n return null;\n};\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n if (!closedDetails) {\n return elementIsVisible;\n }\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n if (summary === null) {\n return false;\n }\n }\n return elementIsVisible;\n};\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n if (element.classList.contains('disabled')) {\n return true;\n }\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n if (element instanceof ShadowRoot) {\n return element;\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null;\n }\n return findShadowRoot(element.parentNode);\n};\nconst noop = () => {};\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\n\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n return null;\n};\nconst DOMContentLoadedCallbacks = [];\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\nconst isRTL = () => document.documentElement.dir === 'rtl';\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n};\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement);\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n index += shouldGetNext ? 1 : -1;\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n return fn.apply(element, [event]);\n };\n}\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n hydrateObj(event, {\n delegateTarget: target\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n return fn.apply(target, [event]);\n }\n }\n };\n}\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string';\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n return [isDelegated, callable, typeEvent];\n}\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n callable = wrapFunction(callable);\n }\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n if (!fn) {\n return;\n }\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n const evt = hydrateObj(new Event(event, {\n bubbles,\n cancelable: true\n }), args);\n if (defaultPrevented) {\n evt.preventDefault();\n }\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n return evt;\n }\n};\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value;\n }\n });\n }\n }\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n if (value === 'false') {\n return false;\n }\n if (value === Number(value).toString()) {\n return Number(value);\n }\n if (value === '' || value === 'null') {\n return null;\n }\n if (typeof value !== 'string') {\n return value;\n }\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n return attributes;\n },\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n static get DefaultType() {\n return {};\n }\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n return config;\n }\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.2';\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n if (!element) {\n return;\n }\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n static get VERSION() {\n return VERSION;\n }\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href');\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n selector = hrefAttribute && hrefAttribute !== '#' ? parseSelector(hrefAttribute.trim()) : null;\n }\n return selector;\n};\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n return parents;\n },\n prev(element, selector) {\n let previous = element.previousElementSibling;\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n previous = previous.previousElementSibling;\n }\n return [];\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n next = next.nextElementSibling;\n }\n return [];\n },\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n },\n getSelectorFromElement(element) {\n const selector = getSelector(element);\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null;\n }\n return null;\n },\n getElementFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.findOne(selector) : null;\n },\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.find(selector) : [];\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target);\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n if (closeEvent.defaultPrevented) {\n return;\n }\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n }\n\n // Private\n _destroyElement() {\n this._element.remove();\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close');\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n if (!element || !Swipe.isSupported()) {\n return;\n }\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n this._initEvents();\n }\n\n // Getters\n static get Default() {\n return Default$c;\n }\n static get DefaultType() {\n return DefaultType$c;\n }\n static get NAME() {\n return NAME$d;\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n this._handleSwipe();\n execute(this._config.endCallback);\n }\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n if (!direction) {\n return;\n }\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n this._addEventListeners();\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$b;\n }\n static get DefaultType() {\n return DefaultType$b;\n }\n static get NAME() {\n return NAME$c;\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT);\n }\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n prev() {\n this._slide(ORDER_PREV);\n }\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n this._clearInterval();\n }\n cycle() {\n this._clearInterval();\n this._updateInterval();\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n this.cycle();\n }\n to(index) {\n const items = this._getItems();\n if (index > items.length - 1 || index < 0) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n const activeIndex = this._getItemIndex(this._getActive());\n if (activeIndex === index) {\n return;\n }\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n this._slide(order, items[index]);\n }\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause();\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n const direction = KEY_TO_DIRECTION[event.key];\n if (direction) {\n event.preventDefault();\n this._slide(this._directionToOrder(direction));\n }\n }\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n if (!element) {\n return;\n }\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n const activeElement = this._getActive();\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n if (nextElement === activeElement) {\n return;\n }\n const nextElementIndex = this._getItemIndex(nextElement);\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n const slideEvent = triggerEvent(EVENT_SLIDE);\n if (slideEvent.defaultPrevented) {\n return;\n }\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return;\n }\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n this._setActiveIndicatorElement(nextElementIndex);\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n if (isCycling) {\n this.cycle();\n }\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n if (slideIndex) {\n carousel.to(slideIndex);\n carousel._maybeEnableCycle();\n return;\n }\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n carousel._maybeEnableCycle();\n return;\n }\n carousel.prev();\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n this._initializeChildren();\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n if (this._config.toggle) {\n this.toggle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$a;\n }\n static get DefaultType() {\n return DefaultType$a;\n }\n static get NAME() {\n return NAME$b;\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n let activeChildren = [];\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n const dimension = this._getDimension();\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.style[dimension] = 0;\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n this._queueCallback(complete, this._element, true);\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n const dimension = this._getDimension();\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger);\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n this._element.style[dimension] = '';\n this._queueCallback(complete, this._element, true);\n }\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n config.parent = getElement(config.parent);\n return config;\n }\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element);\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {};\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n }\n\n // Getters\n static get Default() {\n return Default$9;\n }\n static get DefaultType() {\n return DefaultType$9;\n }\n static get NAME() {\n return NAME$a;\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n if (showEvent.defaultPrevented) {\n return;\n }\n this._createPopper();\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n this._element.focus();\n this._element.setAttribute('aria-expanded', true);\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n this._element.classList.add(CLASS_NAME_SHOW$6);\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n this._completeHide(relatedTarget);\n }\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n super.dispose();\n }\n update() {\n this._inNavbar = this._detectNavbar();\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n if (this._popper) {\n this._popper.destroy();\n }\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n this._element.setAttribute('aria-expanded', 'false');\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n _getConfig(config) {\n config = super._getConfig(config);\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n return config;\n }\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n let referenceElement = this._element;\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n const popperConfig = this._getPopperConfig();\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n _getPlacement() {\n const parentDropdown = this._parent;\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n };\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n if (!items.length) {\n return;\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n if (!context || context._config.autoClose === false) {\n continue;\n }\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n const relatedTarget = {\n relatedTarget: context._element\n };\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n context._completeHide(relatedTarget);\n }\n }\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n if (isInput && !isEscapeEvent) {\n return;\n }\n event.preventDefault();\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n instance._selectMenuItem(event);\n return;\n }\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n};\n\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n }\n\n // Getters\n static get Default() {\n return Default$8;\n }\n static get DefaultType() {\n return DefaultType$8;\n }\n static get NAME() {\n return NAME$9;\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._append();\n const element = this._getElement();\n if (this._config.isAnimated) {\n reflow(element);\n }\n element.classList.add(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n dispose() {\n if (!this._isAppended) {\n return;\n }\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n this._element.remove();\n this._isAppended = false;\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n this._element = backdrop;\n }\n return this._element;\n }\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n _append() {\n if (this._isAppended) {\n return;\n }\n const element = this._getElement();\n this._config.rootElement.append(element);\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n};\n\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n }\n\n // Getters\n static get Default() {\n return Default$7;\n }\n static get DefaultType() {\n return DefaultType$7;\n }\n static get NAME() {\n return NAME$8;\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return;\n }\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n deactivate() {\n if (!this._isActive) {\n return;\n }\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n }\n\n // Private\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n const elements = SelectorEngine.focusableChildren(trapElement);\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n hide() {\n const width = this.getWidth();\n this._disableOverFlow();\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n isOverflowing() {\n return this.getWidth() > 0;\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n this._element.style.overflow = 'hidden';\n }\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n this._saveInitialAttribute(element, styleProperty);\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty);\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$6;\n }\n static get DefaultType() {\n return DefaultType$6;\n }\n static get NAME() {\n return NAME$7;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._isTransitioning = true;\n this._scrollBar.hide();\n document.body.classList.add(CLASS_NAME_OPEN);\n this._adjustDialog();\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._isShown = false;\n this._isTransitioning = true;\n this._focustrap.deactivate();\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n dispose() {\n EventHandler.off(window, EVENT_KEY$4);\n EventHandler.off(this._dialog, EVENT_KEY$4);\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n handleUpdate() {\n this._adjustDialog();\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n this._element.style.display = 'block';\n this._element.removeAttribute('aria-hidden');\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_SHOW$4);\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n return;\n }\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n _hideModal() {\n this._element.style.display = 'none';\n this._element.setAttribute('aria-hidden', true);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n this._isTransitioning = false;\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n this._resetAdjustments();\n this._scrollBar.reset();\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n if (hideEvent.defaultPrevented) {\n return;\n }\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY;\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n this._element.classList.add(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n this._element.focus();\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const scrollbarWidth = this._scrollBar.getWidth();\n const isBodyOverflowing = scrollbarWidth > 0;\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](relatedTarget);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n });\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$5;\n }\n static get DefaultType() {\n return DefaultType$5;\n }\n static get NAME() {\n return NAME$6;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._backdrop.show();\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n this._element.classList.add(CLASS_NAME_SHOW$3);\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n this._queueCallback(completeCallBack, this._element, true);\n }\n hide() {\n if (!this._isShown) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._focustrap.deactivate();\n this._element.blur();\n this._isShown = false;\n this._element.classList.add(CLASS_NAME_HIDING);\n this._backdrop.hide();\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n this._queueCallback(completeCallback, this._element, true);\n }\n dispose() {\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n this.hide();\n };\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n });\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n });\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\n// js-docs-end allow-list\n\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n }\n return true;\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n }\n\n // Getters\n static get Default() {\n return Default$4;\n }\n static get DefaultType() {\n return DefaultType$4;\n }\n static get NAME() {\n return NAME$5;\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n hasContent() {\n return this.getContent().length > 0;\n }\n changeContent(content) {\n this._checkContent(content);\n this._config.content = {\n ...this._config.content,\n ...content\n };\n return this;\n }\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n const template = templateWrapper.children[0];\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n return template;\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n this._checkContent(config.content);\n }\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n if (!templateElement) {\n return;\n }\n content = this._resolvePossibleFunction(content);\n if (!content) {\n templateElement.remove();\n return;\n }\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n return;\n }\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n templateElement.textContent = content;\n }\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this]);\n }\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n templateElement.textContent = element.textContent;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n super(element, config);\n\n // Private\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null;\n\n // Protected\n this.tip = null;\n this._setListeners();\n if (!this._config.selector) {\n this._fixTitle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$3;\n }\n static get DefaultType() {\n return DefaultType$3;\n }\n static get NAME() {\n return NAME$4;\n }\n\n // Public\n enable() {\n this._isEnabled = true;\n }\n disable() {\n this._isEnabled = false;\n }\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n this._activeTrigger.click = !this._activeTrigger.click;\n if (this._isShown()) {\n this._leave();\n return;\n }\n this._enter();\n }\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n this._disposePopper();\n super.dispose();\n }\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper();\n const tip = this._getTipElement();\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n const {\n container\n } = this._config;\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n if (this._isHovered === false) {\n this._leave();\n }\n this._isHovered = false;\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n hide() {\n if (!this._isShown()) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n if (hideEvent.defaultPrevented) {\n return;\n }\n const tip = this._getTipElement();\n tip.classList.remove(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n if (!this._isHovered) {\n this._disposePopper();\n }\n this._element.removeAttribute('aria-describedby');\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n update() {\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n return this.tip;\n }\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml();\n\n // TODO: remove this check in v6\n if (!tip) {\n return null;\n }\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n return tip;\n }\n setContent(content) {\n this._newContent = content;\n if (this._isShown()) {\n this._disposePopper();\n this.show();\n }\n }\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n return this._templateFactory;\n }\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element]);\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element]);\n }\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n context._leave();\n });\n }\n }\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n _fixTitle() {\n const title = this._element.getAttribute('title');\n if (!title) {\n return;\n }\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title');\n }\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n this._isHovered = true;\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n this._isHovered = false;\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n return config;\n }\n _getDelegateConfig() {\n const config = {};\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value;\n }\n }\n config.selector = false;\n config.trigger = 'manual';\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config;\n }\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n this._popper = null;\n }\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n static get DefaultType() {\n return DefaultType$2;\n }\n static get NAME() {\n return NAME$3;\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent();\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n }\n\n // Getters\n static get Default() {\n return Default$1;\n }\n static get DefaultType() {\n return DefaultType$1;\n }\n static get NAME() {\n return NAME$2;\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables();\n this._maybeEnableSmoothScroll();\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n dispose() {\n this._observer.disconnect();\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body;\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n return config;\n }\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height;\n }\n });\n }\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n this._process(targetElement(entry));\n };\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n this._clearActiveClass(targetElement(entry));\n continue;\n }\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry);\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return;\n }\n continue;\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor);\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n this._clearActiveClass(this._config.target);\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n this._activateParents(target);\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both
@@ -940,17 +644,12 @@

Subpackages#

-
-

Module contents#

+
+

Module contents#

@@ -961,7 +660,7 @@

Submodules +