chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:36:30 +08:00
commit 55ab4e4a73
473 changed files with 72932 additions and 0 deletions
+266
View File
@@ -0,0 +1,266 @@
import json
import requests
import fastjsonschema
from copy import deepcopy
from typing import Optional, Union, List, Dict, Any
from pathlib import Path
from easygraph.classes.hypergraph import Hypergraph
schema_url = "https://raw.githubusercontent.com/pszufe/HIF_validators/main/schemas/hif_schema_v0.1.0.json"
class EasyGraphHIFError(Exception):
"""Custom exception for HIF conversion errors."""
pass
_hif_validator = None
def _get_hif_validator():
global _hif_validator
if _hif_validator is None:
try:
resp = requests.get(schema_url, timeout=5)
if resp.status_code == 200:
schema = json.loads(resp.text)
_hif_validator = fastjsonschema.compile(schema)
except Exception:
print("Warning: HIF Schema could not be fetched. Validation skipped.")
_hif_validator = lambda x: True
return _hif_validator if _hif_validator else (lambda x: True)
def hypergraph_to_hif(
hg: Hypergraph,
filename: Optional[Union[str, Path]] = None,
node_label: str = "name",
edge_label: str = "name",
) -> dict:
"""
Converts an EasyGraph Hypergraph to HIF JSON.
Correctly handles hg.e tuple structure ((edges), (weights), (props)).
"""
if hasattr(hg, "custom_hif_nodes"):
nodj = hg.custom_hif_nodes
else:
nodj = []
num_v = hg.num_v if hasattr(hg, "num_v") else len(hg.v_property) if hasattr(hg, "v_property") else 0
v_props = getattr(hg, "v_property", [{} for _ in range(num_v)])
if not v_props and num_v > 0: v_props = [{} for _ in range(num_v)]
for i in range(num_v):
props = v_props[i] if i < len(v_props) and isinstance(v_props[i], dict) else {}
p = props.copy()
weight = p.pop("weight", 1.0)
if node_label in p:
node_id = str(p.get(node_label))
if node_label == "name":
p.pop("name", None)
else:
node_id = p.pop("name", str(i))
nodj.append({"node": node_id, "weight": weight, "attrs": p})
e_structure = []
e_weights = []
e_props = []
if hasattr(hg, "e") and isinstance(hg.e, tuple) and len(hg.e) == 3 and \
isinstance(hg.e[0], (list, tuple)) and isinstance(hg.e[1], (list, tuple)):
e_structure = hg.e[0]
e_weights = hg.e[1]
e_props = hg.e[2]
elif hasattr(hg, "e_list") and hg.e_list:
e_structure = hg.e_list
e_weights = getattr(hg, "e_weight", [1.0] * len(e_structure))
e_props = getattr(hg, "e_property_full", [{} for _ in range(len(e_structure))])
elif hasattr(hg, "e") and isinstance(hg.e, (list, tuple)):
e_structure = hg.e
e_weights = getattr(hg, "e_weight", [1.0] * len(e_structure))
e_props = getattr(hg, "e_property_full", [{} for _ in range(len(e_structure))])
num_e = len(e_structure)
if len(e_weights) < num_e: e_weights = [1.0] * num_e
if len(e_props) < num_e: e_props = [{} for _ in range(num_e)]
if hasattr(hg, "custom_hif_edges"):
edgj = hg.custom_hif_edges
else:
edgj = []
for i in range(num_e):
props = e_props[i].copy() if isinstance(e_props[i], dict) else {}
# edge_id = props.pop("name", str(i))
weight = e_weights[i]
props.pop("weight", None)
if edge_label in props:
edge_id = str(props.get(edge_label))
if edge_label == "name":
props.pop("name", None)
else:
edge_id = props.pop("name", str(i))
edgj.append({"edge": edge_id, "weight": weight, "attrs": props})
if hasattr(hg, "custom_hif_incidences"):
incj = hg.custom_hif_incidences
else:
incj = []
node_id_list = [n["node"] for n in nodj]
edge_id_list = [e["edge"] for e in edgj]
for e_idx, nodes_in_edge in enumerate(e_structure):
if e_idx >= len(edge_id_list): break
edge_name = edge_id_list[e_idx]
flat_nodes = []
if isinstance(nodes_in_edge, (list, tuple)):
for item in nodes_in_edge:
if isinstance(item, (list, tuple)):
flat_nodes.extend(item)
else:
flat_nodes.append(item)
else:
flat_nodes = [nodes_in_edge]
for n_idx in flat_nodes:
try:
n_idx_int = int(n_idx)
if 0 <= n_idx_int < len(node_id_list):
incj.append({
"edge": edge_name,
"node": node_id_list[n_idx_int],
"weight": 1.0,
})
except (ValueError, TypeError):
continue
metadata = getattr(hg, "metadata", {})
network_type = getattr(hg, "network_type", "undirected")
hif = {
"nodes": nodj,
"edges": edgj,
"incidences": incj,
"network-type": network_type,
"metadata": metadata
}
try:
validator = _get_hif_validator()
validator(hif)
except Exception as e:
print(f"Validation Warning: {e}")
if filename:
with open(filename, "w", encoding='utf-8') as f:
json.dump(hif, f, indent=4, ensure_ascii=False)
return hif
def hif_to_hypergraph(
hif: dict = None,
filename: Optional[Union[str, Path]] = None,
node_label: str = "name",
edge_label: str = "name",
):
"""
Reads HIF JSON and returns an EasyGraph Hypergraph.
Attaches original JSON parts to 'custom_hif_*' attributes to preserve
structure during round-trips.
"""
if hif is None:
if filename is None:
raise EasyGraphHIFError("No HIF data or filename provided.")
try:
with open(filename, "r", encoding='utf-8') as f:
hif = json.load(f)
except Exception as e:
raise EasyGraphHIFError(f"Failed to load HIF file {filename}: {e}")
nodes_list = hif.get("nodes", [])
node_name_to_idx = {rec["node"]: i for i, rec in enumerate(nodes_list)}
num_v = len(nodes_list)
edges_list = hif.get("edges", [])
edge_name_to_idx = {rec["edge"]: i for i, rec in enumerate(edges_list)}
num_e = len(edges_list)
v_property = [{} for _ in range(num_v)]
for rec in nodes_list:
idx = node_name_to_idx.get(rec["node"])
if idx is not None:
prop = rec.get("attrs", {}).copy()
if node_label in prop:
prop["name"] = str(prop[node_label])
else:
prop["name"] = rec["node"]
prop["weight"] = rec.get("weight", 1.0)
v_property[idx] = prop
e_property_full = [{} for _ in range(num_e)]
e_weight = [1.0] * num_e
for rec in edges_list:
idx = edge_name_to_idx.get(rec["edge"])
if idx is not None:
prop = rec.get("attrs", {}).copy()
# if "name" not in prop:
# prop["name"] = rec["edge"]
if edge_label in prop:
prop["name"] = str(prop[edge_label])
else:
prop["name"] = rec["edge"]
prop["weight"] = rec.get("weight", 1.0)
e_property_full[idx] = prop
e_weight[idx] = prop["weight"]
raw_groups = [[] for _ in range(num_e)]
incidences_list = hif.get("incidences", [])
for inc in incidences_list:
e_name = inc.get("edge")
n_name = inc.get("node")
e_idx = edge_name_to_idx.get(e_name)
n_idx = node_name_to_idx.get(n_name)
if e_idx is not None and n_idx is not None:
raw_groups[e_idx].append(n_idx)
hg = Hypergraph(
num_v=num_v,
e_list=raw_groups,
e_weight=e_weight,
v_property=v_property
)
hg.node_label_index = {}
for i in range(num_v):
name = v_property[i].get("name")
if name:
hg.node_label_index[name] = i
hg.edge_label_index = {}
for i in range(num_e):
name = e_property_full[i].get("name")
if name:
hg.edge_label_index[name] = i
hg.custom_hif_nodes = deepcopy(nodes_list)
hg.custom_hif_edges = deepcopy(edges_list)
hg.custom_hif_incidences = deepcopy(incidences_list)
if "metadata" in hif:
hg.metadata = deepcopy(hif["metadata"])
else:
hg.metadata = {}
if "network-type" in hif:
hg.network_type = hif["network-type"]
hg.e_property_full = e_property_full
return hg
+14
View File
@@ -0,0 +1,14 @@
from easygraph.utils.alias import *
from easygraph.utils.convert_class import *
from easygraph.utils.convert_to_matrix import *
from easygraph.utils.decorators import *
from easygraph.utils.download import *
from easygraph.utils.exception import *
from easygraph.utils.index_of_node import *
from easygraph.utils.logging import *
from easygraph.utils.mapped_queue import *
from easygraph.utils.misc import *
from easygraph.utils.relabel import *
from easygraph.utils.sparse import *
from easygraph.utils.type_change import *
from easygraph.utils.HIF import *
+119
View File
@@ -0,0 +1,119 @@
__all__ = ["create_alias_table", "alias_sample", "alias_setup", "alias_draw"]
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
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]
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]
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
+19
View File
@@ -0,0 +1,19 @@
__all__ = [
"convert_graph_class",
]
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
import hashlib
import warnings
from functools import wraps
from pathlib import Path
import requests
__all__ = [
"check_file",
"download_file",
"download_and_check",
]
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)
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
@_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
+43
View File
@@ -0,0 +1,43 @@
"""
**********
Exceptions
**********
Base exceptions and errors for EasyGraph.
"""
__all__ = [
"EasyGraphException",
"EasyGraphError",
"EasyGraphNotImplemented",
"EasyGraphPointlessConcept",
]
class EasyGraphException(Exception):
"""Base class for exceptions in EasyGraph."""
class EasyGraphError(EasyGraphException):
"""Exception for a serious error in EasyGraph"""
class EasyGraphNotImplemented(EasyGraphException):
"""Exception raised by algorithms not implemented for a type of graph."""
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.
"""
+12
View File
@@ -0,0 +1,12 @@
__all__ = ["get_relation_of_index_and_node"]
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
+44
View File
@@ -0,0 +1,44 @@
import logging
import sys
from pathlib import Path
from typing import Union
__all__ = ["default_log_formatter", "simple_stdout2file"]
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")
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)
+186
View File
@@ -0,0 +1,186 @@
"""
Priority queue class with updatable priorities.
Codes from NetworkX - http://networkx.github.io/
"""
import heapq
__all__ = ["MappedQueue"]
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")
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
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
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)
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
+222
View File
@@ -0,0 +1,222 @@
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"
]
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
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
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
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.
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)
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))
+106
View File
@@ -0,0 +1,106 @@
import easygraph as eg
__all__ = ["relabel_nodes", "convert_node_labels_to_integers"]
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
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
+39
View File
@@ -0,0 +1,39 @@
__all__ = ["sparse_dropout"]
# if not type checking
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import torch
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
+147
View File
@@ -0,0 +1,147 @@
import easygraph as eg
__all__ = [
"from_pyGraphviz_agraph",
"to_pyGraphviz_agraph",
]
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
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