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
+3
View File
@@ -0,0 +1,3 @@
from .drawing import *
from .plot import *
from .positioning import *
+253
View File
@@ -0,0 +1,253 @@
from typing import Any
from typing import List
from typing import Optional
from typing import Union
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
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
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
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 = None,
):
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)
print("len(e_list):", e_list)
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
return v_size, v_line_width, e_line_width, font_size
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,
)
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
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,
)
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
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.")
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.")
def fill_strength(custom_scale: Optional[float], default_value: float):
if custom_scale is None:
return default_value
return custom_scale * default_value
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
import math
from math import pi
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
def vlen(vector):
return math.sqrt(vector[0] ** 2 + vector[1] ** 2)
def common_tangent_radian(r1, r2, d):
if r1 < 0 or r2 < 0:
raise ValueError("Circle radii must be non-negative.")
if d <= 0 or d < abs(r2 - r1):
raise ValueError("No common tangent exists for the given circles.")
value = abs(r2 - r1) / d
if value > 1.0:
value = 1.0
elif value < -1.0:
value = -1.0
alpha = math.acos(value)
alpha = alpha if r1 > r2 else pi - alpha
return alpha
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
def rad_2_deg(rad):
return rad * 180 / pi
+33
View File
@@ -0,0 +1,33 @@
from typing import List
from .simulator import Simulator
from .utils import edge_list_to_incidence_matrix
from .utils import init_pos
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
+233
View File
@@ -0,0 +1,233 @@
import easygraph as eg
__all__ = [
"plot_Followers",
"plot_Connected_Communities",
"plot_Betweenness_Centrality",
"plot_Neighborhood_Followers",
]
# Number of Followers
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
assert len(SHS) < len(
G.nodes
), "The number of SHS must be less than the number of nodes in the graph."
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
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
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)
bc = dict(zip(G.nodes, bc))
sample1 = []
sample2 = []
for i in bc.keys():
if i in OU:
sample1.append(bc[i])
else:
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
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()
+646
View File
@@ -0,0 +1,646 @@
import easygraph as eg
from easygraph.utils.exception import EasyGraphError
__all__ = [
"random_position",
"circular_position",
"shell_position",
"rescale_position",
"kamada_kawai_layout",
# "spring_layout",
# "fruchterman_reingold_layout",
# "_process_params",
# "_fruchterman_reingold",
# "_sparse_fruchterman_reingold",
]
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
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
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
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
assert (
len(pos.shape) != 1
), "One-dimensional ndarray is not available for rescaling."
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
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())
# @np_random_state(10)
# def spring_layout(
# G,
# k=None,
# pos=None,
# fixed=None,
# iterations=50,
# threshold=1e-4,
# weight="weight",
# scale=1,
# center=None,
# dim=2,
# seed=None,
# ):
# """Position nodes using Fruchterman-Reingold force-directed algorithm.
#
# The algorithm simulates a force-directed representation of the network
# treating edges as springs holding nodes close, while treating nodes
# as repelling objects, sometimes called an anti-gravity force.
# Simulation continues until the positions are close to an equilibrium.
#
# There are some hard-coded values: minimal distance between
# nodes (0.01) and "temperature" of 0.1 to ensure nodes don't fly away.
# During the simulation, `k` helps determine the distance between nodes,
# though `scale` and `center` determine the size and place after
# rescaling occurs at the end of the simulation.
#
# Fixing some nodes doesn't allow them to move in the simulation.
# It also turns off the rescaling feature at the simulation's end.
# In addition, setting `scale` to `None` turns off rescaling.
#
# Parameters
# ----------
# G : EasyGraph graph or list of nodes
# A position will be assigned to every node in G.
#
# k : float (default=None)
# Optimal distance between nodes. If None the distance is set to
# 1/sqrt(n) where n is the number of nodes. Increase this value
# to move nodes farther apart.
#
# 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
# random initial positions.
#
# fixed : list or None optional (default=None)
# Nodes to keep fixed at initial position.
# Nodes not in ``G.nodes`` are ignored.
# ValueError raised if `fixed` specified and `pos` not.
#
# iterations : int optional (default=50)
# Maximum number of iterations taken
#
# threshold: float optional (default = 1e-4)
# Threshold for relative error in node position changes.
# The iteration stops if the error is below this threshold.
#
# weight : string or None optional (default='weight')
# The edge attribute that holds the numerical value used for
# the edge weight. Larger means a stronger attractive force.
# If None, then all edge weights are 1.
#
# scale : number or None (default: 1)
# Scale factor for positions. Not used unless `fixed is None`.
# If scale is None, no rescaling is performed.
#
# center : array-like or None
# Coordinate pair around which to center the layout.
# Not used unless `fixed is None`.
#
# dim : int
# Dimension of layout.
#
# seed : int, RandomState instance or None optional (default=None)
# Set the random state for deterministic node layouts.
# If int, `seed` is the seed used by the random number generator,
# if numpy.random.RandomState instance, `seed` is the random
# number generator,
# if None, the random number generator is the RandomState instance used
# by numpy.random.
#
# Returns
# -------
# pos : dict
# A dictionary of positions keyed by node
#
# Examples
# --------
# >>> G = eg.path_graph(4)
# >>> pos = eg.spring_layout(G)
#
#
# """
# import numpy as np
#
# G, center = _process_params(G, center, dim)
#
# if fixed is not None:
# if pos is None:
# raise ValueError("nodes are fixed without positions given")
# for node in fixed:
# if node not in pos:
# raise ValueError("nodes are fixed without positions given")
# nfixed = {node: i for i, node in enumerate(G)}
# fixed = np.asarray([nfixed[node] for node in fixed if node in nfixed])
#
# if pos is not None:
# # Determine size of existing domain to adjust initial positions
# dom_size = max(coord for pos_tup in pos.values() for coord in pos_tup)
# if dom_size == 0:
# dom_size = 1
# pos_arr = seed.rand(len(G), dim) * dom_size + center
#
# for i, n in enumerate(G):
# if n in pos:
# pos_arr[i] = np.asarray(pos[n])
# else:
# pos_arr = None
# dom_size = 1
#
# if len(G) == 0:
# return {}
# if len(G) == 1:
# return {eg.utils.arbitrary_element(G.nodes()): center}
#
# try:
# # Sparse matrix
# if len(G) < 500: # sparse solver for large graphs
# raise ValueError
# A = eg.to_scipy_sparse_array(G, weight=weight, dtype="f")
# if k is None and fixed is not None:
# # We must adjust k by domain size for layouts not near 1x1
# nnodes, _ = A.shape
# k = dom_size / np.sqrt(nnodes)
# pos = _sparse_fruchterman_reingold(
# A, k, pos_arr, fixed, iterations, threshold, dim, seed
# )
# except ValueError:
# A = eg.to_numpy_array(G, weight=weight)
# if k is None and fixed is not None:
# # We must adjust k by domain size for layouts not near 1x1
# nnodes, _ = A.shape
# k = dom_size / np.sqrt(nnodes)
# pos = _fruchterman_reingold(
# A, k, pos_arr, fixed, iterations, threshold, dim, seed
# )
# if fixed is None and scale is not None:
# pos = rescale_position(pos, scale=scale) + center
# pos = dict(zip(G, pos))
# return pos
#
# fruchterman_reingold_layout = spring_layout
#
# def _process_params(G, center, dim):
# # Some boilerplate code.
# import numpy as np
#
# if not isinstance(G, eg.Graph):
# empty_graph = eg.Graph()
# empty_graph.add_nodes_from(G)
# G = empty_graph
#
# 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)
#
# return G, center
#
# @np_random_state(7)
# def _fruchterman_reingold(
# A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None
# ):
# # Position nodes in adjacency matrix A using Fruchterman-Reingold
# # Entry point for NetworkX graph is fruchterman_reingold_layout()
# import numpy as np
#
# try:
# nnodes, _ = A.shape
# except AttributeError as err:
# msg = "fruchterman_reingold() takes an adjacency matrix as input"
# raise EasyGraphError(msg) from err
#
# if pos is None:
# # random initial positions
# pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
# else:
# # make sure positions are of same type as matrix
# pos = pos.astype(A.dtype)
#
# # optimal distance between nodes
# if k is None:
# k = np.sqrt(1.0 / nnodes)
# # the initial "temperature" is about .1 of domain area (=1x1)
# # this is the largest step allowed in the dynamics.
# # We need to calculate this in case our fixed positions force our domain
# # to be much bigger than 1x1
# t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
# # simple cooling scheme.
# # linearly step down by dt on each iteration so last iteration is size dt.
# dt = t / (iterations + 1)
# delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype)
# # the inscrutable (but fast) version
# # this is still O(V^2)
# # could use multilevel methods to speed this up significantly
# for iteration in range(iterations):
# # matrix of difference between points
# delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
# # distance between points
# distance = np.linalg.norm(delta, axis=-1)
# # enforce minimum distance of 0.01
# np.clip(distance, 0.01, None, out=distance)
# # displacement "force"
# displacement = np.einsum(
# "ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k)
# )
# # update positions
# length = np.linalg.norm(displacement, axis=-1)
# length = np.where(length < 0.01, 0.1, length)
# delta_pos = np.einsum("ij,i->ij", displacement, t / length)
# if fixed is not None:
# # don't change positions of fixed nodes
# delta_pos[fixed] = 0.0
# pos += delta_pos
# # cool temperature
# t -= dt
# if (np.linalg.norm(delta_pos) / nnodes) < threshold:
# break
# return pos
#
# @np_random_state(7)
# def _sparse_fruchterman_reingold(
# A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None
# ):
# # Position nodes in adjacency matrix A using Fruchterman-Reingold
# # Entry point for NetworkX graph is fruchterman_reingold_layout()
# # Sparse version
# import numpy as np
# import scipy as sp
# import scipy.sparse # call as sp.sparse
#
# try:
# nnodes, _ = A.shape
# except AttributeError as err:
# msg = "fruchterman_reingold() takes an adjacency matrix as input"
# raise EasyGraphError(msg) from err
# # make sure we have a LIst of Lists representation
# try:
# A = A.tolil()
# except AttributeError:
# A = (sp.sparse.coo_array(A)).tolil()
#
# if pos is None:
# # random initial positions
# pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
# else:
# # make sure positions are of same type as matrix
# pos = pos.astype(A.dtype)
#
# # no fixed nodes
# if fixed is None:
# fixed = []
#
# # optimal distance between nodes
# if k is None:
# k = np.sqrt(1.0 / nnodes)
# # the initial "temperature" is about .1 of domain area (=1x1)
# # this is the largest step allowed in the dynamics.
# t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
# # simple cooling scheme.
# # linearly step down by dt on each iteration so last iteration is size dt.
# dt = t / (iterations + 1)
#
# displacement = np.zeros((dim, nnodes))
# for iteration in range(iterations):
# displacement *= 0
# # loop over rows
# for i in range(A.shape[0]):
# if i in fixed:
# continue
# # difference between this row's node position and all others
# delta = (pos[i] - pos).T
# # distance between points
# distance = np.sqrt((delta**2).sum(axis=0))
# # enforce minimum distance of 0.01
# distance = np.where(distance < 0.01, 0.01, distance)
# # the adjacency matrix row
# Ai = A.getrowview(i).toarray() # TODO: revisit w/ sparse 1D container
# # displacement "force"
# displacement[:, i] += (
# delta * (k * k / distance**2 - Ai * distance / k)
# ).sum(axis=1)
# # update positions
# length = np.sqrt((displacement**2).sum(axis=0))
# length = np.where(length < 0.01, 0.1, length)
# delta_pos = (displacement * t / length).T
# pos += delta_pos
# # cool temperature
# t -= dt
# if (np.linalg.norm(delta_pos) / nnodes) < threshold:
# break
# return pos
+195
View File
@@ -0,0 +1,195 @@
from copy import deepcopy
from .utils import safe_div
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
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
)
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, min_dist=1e-6):
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 = 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
@@ -0,0 +1,27 @@
import unittest
import easygraph as eg
class TestGeometry(unittest.TestCase):
def setUp(self):
self.G = eg.datasets.get_graph_karateclub()
def test_overall(self):
eg.draw_SHS_center(self.G, [1, 33, 34], style="side")
eg.draw_SHS_center(self.G, [1, 33, 34], style="center")
eg.draw_SHS_center_kk(self.G, [1, 33, 34], style="side")
eg.draw_SHS_center_kk(self.G, [1, 33, 34], style="center")
eg.draw_kamada_kawai(self.G, style="side")
eg.draw_kamada_kawai(self.G, style="center")
eg.draw_SHS_center(self.G, [1, 33, 34], rate=0.8, style="side")
eg.draw_SHS_center(self.G, [1, 33, 34], rate=0.8, style="center")
eg.draw_SHS_center_kk(self.G, [1, 33, 34], rate=0.8, style="side")
eg.draw_SHS_center_kk(self.G, [1, 33, 34], rate=0.8, style="center")
eg.draw_kamada_kawai(self.G, rate=0.8, style="side")
eg.draw_kamada_kawai(self.G, rate=0.8, style="center")
if __name__ == "__main__":
unittest.main()
# pretty awesome images
@@ -0,0 +1,78 @@
import math
import unittest
import numpy as np
from easygraph.functions.drawing.geometry import common_tangent_radian
from easygraph.functions.drawing.geometry import polar_position
from easygraph.functions.drawing.geometry import rad_2_deg
from easygraph.functions.drawing.geometry import radian_from_atan
from easygraph.functions.drawing.geometry import vlen
class TestGeometryUtils(unittest.TestCase):
def test_radian_from_atan_axes(self):
self.assertAlmostEqual(radian_from_atan(0, 1), math.pi / 2)
self.assertAlmostEqual(radian_from_atan(0, -1), 3 * math.pi / 2)
self.assertAlmostEqual(radian_from_atan(1, 0), 0)
self.assertAlmostEqual(radian_from_atan(-1, 0), math.pi)
def test_radian_from_atan_quadrants(self):
# Q1
self.assertAlmostEqual(radian_from_atan(1, 1), math.atan(1))
# Q4
self.assertAlmostEqual(radian_from_atan(1, -1), math.atan(-1) + 2 * math.pi)
# Q2
self.assertAlmostEqual(radian_from_atan(-1, 1), math.atan(-1) + math.pi)
# Q3
self.assertAlmostEqual(radian_from_atan(-1, -1), math.atan(1) + math.pi)
def test_radian_from_atan_zero_vector(self):
result = radian_from_atan(0, 0)
self.assertAlmostEqual(result, 3 * math.pi / 2)
def test_vlen(self):
self.assertEqual(vlen((3, 4)), 5.0)
self.assertEqual(vlen((0, 0)), 0.0)
self.assertAlmostEqual(vlen((-3, -4)), 5.0)
def test_common_tangent_radian_basic(self):
r1, r2, d = 3, 2, 5
angle = common_tangent_radian(r1, r2, d)
expected = math.acos(abs(r2 - r1) / d)
self.assertAlmostEqual(angle, expected)
def test_common_tangent_radian_reversed(self):
r1, r2, d = 2, 3, 5
angle = common_tangent_radian(r1, r2, d)
expected = math.pi - math.acos(abs(r2 - r1) / d)
self.assertAlmostEqual(angle, expected)
def test_common_tangent_radian_touching(self):
self.assertAlmostEqual(common_tangent_radian(3, 3, 5), math.pi / 2)
def test_common_tangent_radian_invalid(self):
with self.assertRaises(ValueError):
common_tangent_radian(5, 1, 2)
def test_polar_position_origin(self):
pos = polar_position(0, 0, np.array([5, 5]))
np.testing.assert_array_almost_equal(pos, np.array([5, 5]))
def test_polar_position_90deg(self):
pos = polar_position(1, math.pi / 2, np.array([0, 0]))
np.testing.assert_array_almost_equal(pos, np.array([0, 1]))
def test_polar_position_negative_angle(self):
pos = polar_position(1, -math.pi / 2, np.array([1, 1]))
np.testing.assert_array_almost_equal(pos, np.array([1, 0]))
def test_rad_2_deg(self):
self.assertEqual(rad_2_deg(0), 0)
self.assertEqual(rad_2_deg(math.pi), 180)
self.assertEqual(rad_2_deg(2 * math.pi), 360)
self.assertEqual(rad_2_deg(-math.pi / 2), -90)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,39 @@
import unittest
import easygraph as eg
import matplotlib.pyplot as plt
import numpy as np
import statsmodels.api as sm
class TestPlot(unittest.TestCase):
def setUp(self):
self.ds = eg.datasets.get_graph_karateclub()
self.edges = [
(1, 4),
(2, 4),
("String", "Bool"),
(4, 1),
(0, 4),
(4, 256),
((1, 2), (3, 4)),
]
self.test_graphs = [eg.Graph(), eg.DiGraph()]
self.test_graphs.append(eg.classes.DiGraph(self.edges))
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
def test_plot_Followers(self):
eg.functions.plot_Followers(self.ds, self.shs)
def test_plot_Connected_Communities(self):
eg.functions.plot_Connected_Communities(self.ds, self.shs)
def test_plot_Neighborhood_Followers(self):
eg.functions.plot_Neighborhood_Followers(self.ds, self.shs)
def test_plot_Betweenness_Centrality(self):
eg.functions.plot_Betweenness_Centrality(self.ds, self.shs)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,56 @@
import unittest
import easygraph as eg
import matplotlib.pyplot as plt
import numpy as np
class TestPositioning(unittest.TestCase):
def setUp(self):
self.ds = eg.datasets.get_graph_karateclub()
self.edges = [
(1, 4),
(2, 4),
("String", "Bool"),
(4, 1),
(0, 4),
(4, 256),
((1, 2), (3, 4)),
]
self.test_graphs = [eg.Graph(), eg.DiGraph()]
self.test_graphs.append(eg.classes.DiGraph(self.edges))
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
def test_random_position(self):
print()
for i in self.test_graphs:
print(eg.random_position(i))
def test_circular_position(self):
print()
for i in self.test_graphs:
print(eg.circular_position(i))
def test_shell_position(self):
print()
for i in self.test_graphs:
print(eg.shell_position(i))
def test_rescale_position(self):
print()
for i in self.test_graphs:
try:
pos = eg.random_position(i)
obj = np.array(list(pos.values()))
print(eg.rescale_position(obj))
except Exception as e:
print(e)
def test_kamada_kawai_layout(self):
print()
for i in self.test_graphs:
print(eg.kamada_kawai_layout(i))
if __name__ == "__main__":
unittest.main()
+596
View File
@@ -0,0 +1,596 @@
from itertools import chain
from typing import List
from typing import Optional
from typing import Tuple
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from matplotlib.collections import LineCollection
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
# from fa2 import ForceAtlas2
# import bezier
# import numpy as np
# from easygraph import to_networkx
# from easygraph.utils.exception import EasyGraphError
# import easygraph as eg
def safe_div(a: np.ndarray, b: np.ndarray, jitter_scale: float = 0.000001):
mask = b == 0
b[mask] = 1
eps = 1e-10
inv_b = np.divide(1.0, np.maximum(b, eps))
res = a * inv_b
if mask.sum() > 0:
res[mask.repeat(2, 2)] = np.random.randn(mask.sum() * 2) * jitter_scale
return res
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
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,
)
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]
patcjes_arc = 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],
edgecolor=e_color[eidx],
facecolor=e_fill_color[eidx],
)
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],
edgecolor=e_color[eidx],
facecolor=e_fill_color[eidx],
)
)
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
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,
edgecolors,
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)
edgecolors = "black" if edgecolors == None else edgecolors
p = PatchCollection(patches, facecolors=v_color, edgecolors=edgecolors)
ax.add_collection(p)
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
def apply_alpha(colors, alpha, elem_list, cmap=None, vmin=None, vmax=None):
"""Apply an alpha (or list of alphas) to the colors provided.
Parameters
----------
colors : color string or array of floats (default='r')
Color of element. Can be a single color format string,
or a sequence of colors with the same length as nodelist.
If numeric values are specified they will be mapped to
colors using the cmap and vmin,vmax parameters. See
matplotlib.scatter for more details.
alpha : float or array of floats
Alpha values for elements. This can be a single alpha value, in
which case it will be applied to all the elements of color. Otherwise,
if it is an array, the elements of alpha will be applied to the colors
in order (cycling through alpha multiple times if necessary).
elem_list : array of networkx objects
The list of elements which are being colored. These could be nodes,
edges or labels.
cmap : matplotlib colormap
Color map for use if colors is a list of floats corresponding to points
on a color mapping.
vmin, vmax : float
Minimum and maximum values for normalizing colors if a colormap is used
Returns
-------
rgba_colors : numpy ndarray
Array containing RGBA format values for each of the node colours.
"""
from itertools import cycle
from itertools import islice
from numbers import Number
import matplotlib as mpl
import matplotlib.cm # call as mpl.cm
import matplotlib.colors # call as mpl.colors
import numpy as np
# If we have been provided with a list of numbers as long as elem_list,
# apply the color mapping.
if len(colors) == len(elem_list) and isinstance(colors[0], Number):
mapper = mpl.cm.ScalarMappable(cmap=cmap)
mapper.set_clim(vmin, vmax)
rgba_colors = mapper.to_rgba(colors)
# Otherwise, convert colors to matplotlib's RGB using the colorConverter
# object. These are converted to numpy ndarrays to be consistent with the
# to_rgba method of ScalarMappable.
else:
try:
rgba_colors = np.array([mpl.colors.colorConverter.to_rgba(colors)])
except ValueError:
rgba_colors = np.array(
[mpl.colors.colorConverter.to_rgba(color) for color in colors]
)
# Set the final column of the rgba_colors to have the relevant alpha values
try:
# If alpha is longer than the number of colors, resize to the number of
# elements. Also, if rgba_colors.size (the number of elements of
# rgba_colors) is the same as the number of elements, resize the array,
# to avoid it being interpreted as a colormap by scatter()
if len(alpha) > len(rgba_colors) or rgba_colors.size == len(elem_list):
rgba_colors = np.resize(rgba_colors, (len(elem_list), 4))
rgba_colors[1:, 0] = rgba_colors[0, 0]
rgba_colors[1:, 1] = rgba_colors[0, 1]
rgba_colors[1:, 2] = rgba_colors[0, 2]
rgba_colors[:, 3] = list(islice(cycle(alpha), len(rgba_colors)))
except TypeError:
rgba_colors[:, -1] = alpha
return rgba_colors
# def draw_easygraph_nodes(
# G,
# pos,
# nodelist=None,
# node_size=300,
# node_color="#1f78b4",
# node_shape="o",
# alpha=None,
# cmap=None,
# vmin=None,
# vmax=None,
# ax=None,
# linewidths=None,
# edgecolors=None,
# label=None,
# margins=None,
# ):
# """Draw the nodes of the graph G.
# This draws only the nodes of the graph G.
# Parameters
# ----------
# G : graph
# A easygraph graph
# pos : dictionary
# A dictionary with nodes as keys and positions as values.
# Positions should be sequences of length 2.
# ax : Matplotlib Axes object, optional
# Draw the graph in the specified Matplotlib axes.
# nodelist : list (default list(G))
# Draw only specified nodes
# node_size : scalar or array (default=300)
# Size of nodes. If an array it must be the same length as nodelist.
# node_color : color or array of colors (default='#1f78b4')
# Node color. Can be a single color or a sequence of colors with the same
# length as nodelist. Color can be string or rgb (or rgba) tuple of
# floats from 0-1. If numeric values are specified they will be
# mapped to colors using the cmap and vmin,vmax parameters. See
# matplotlib.scatter for more details.
# node_shape : string (default='o')
# The shape of the node. Specification is as matplotlib.scatter
# marker, one of 'so^>v<dph8'.
# alpha : float or array of floats (default=None)
# The node transparency. This can be a single alpha value,
# in which case it will be applied to all the nodes of color. Otherwise,
# if it is an array, the elements of alpha will be applied to the colors
# in order (cycling through alpha multiple times if necessary).
# cmap : Matplotlib colormap (default=None)
# Colormap for mapping intensities of nodes
# vmin,vmax : floats or None (default=None)
# Minimum and maximum for node colormap scaling
# linewidths : [None | scalar | sequence] (default=1.0)
# Line width of symbol border
# edgecolors : [None | scalar | sequence] (default = node_color)
# Colors of node borders
# label : [None | string]
# Label for legend
# margins : float or 2-tuple, optional
# Sets the padding for axis autoscaling. Increase margin to prevent
# clipping for nodes that are near the edges of an image. Values should
# be in the range ``[0, 1]``. See :meth:`matplotlib.axes.Axes.margins`
# for details. The default is `None`, which uses the Matplotlib default.
# Returns
# -------
# matplotlib.collections.PathCollection
# `PathCollection` of the nodes.
# Examples
# --------
# >>> from easygraph.datasets import get_graph_karateclub
# >>> import easygraph as eg
# >>> G = get_graph_karateclub()
# >>> nodes = eg.draw_easygraph_nodes(G, pos=eg.circular_position(G))
# """
# from collections.abc import Iterable
# import matplotlib as mpl
# import matplotlib.collections # call as mpl.collections
# import matplotlib.pyplot as plt
# import numpy as np
# if ax is None:
# ax = plt.gca()
# if nodelist is None:
# nodelist = list(G)
# if len(nodelist) == 0: # empty nodelist, no drawing
# return mpl.collections.PathCollection(None)
# try:
# xy = np.asarray([pos[v] for v in nodelist])
# except KeyError as err:
# raise EasyGraphError(f"Node {err} has no position.") from err
# if isinstance(alpha, Iterable):
# node_color = apply_alpha(node_color, alpha, nodelist, cmap, vmin, vmax)
# alpha = None
# node_collection = ax.scatter(
# xy[:, 0],
# xy[:, 1],
# s=node_size,
# c=node_color,
# marker=node_shape,
# cmap=cmap,
# vmin=vmin,
# vmax=vmax,
# alpha=alpha,
# linewidths=linewidths,
# edgecolors=edgecolors,
# label=label,
# )
# ax.tick_params(
# axis="both",
# which="both",
# bottom=False,
# left=False,
# labelbottom=False,
# labelleft=False,
# )
# if margins is not None:
# if isinstance(margins, Iterable):
# ax.margins(*margins)
# else:
# ax.margins(margins)
# node_collection.set_zorder(2)
# return node_collection
# def draw_curved_edges(G, pos, dist_ratio=0.2, bezier_precision=20, polarity='random'):
# # Get nodes into np array
# edges = np.array(G.edges())
# l = edges.shape[0]
# if polarity == 'random':
# # Random polarity of curve
# rnd = np.where(np.random.randint(2, size=l)==0, -1, 1)
# else:
# # Create a fixed (hashed) polarity column in the case we use fixed polarity
# # This is useful, e.g., for animations
# rnd = np.where(np.mod(np.vectorize(hash)(edges[:,0])+np.vectorize(hash)(edges[:,1]),2)==0,-1,1)
# # Coordinates (x,y) of both nodes for each edge
# # e.g., https://stackoverflow.com/questions/16992713/translate-every-element-in-numpy-array-according-to-key
# # Note the np.vectorize method doesn't work for all node position dictionaries for some reason
# u, inv = np.unique(edges, return_inverse = True)
# coords = np.array([pos[x] for x in u])[inv].reshape([edges.shape[0], 2, edges.shape[1]])
# coords_node1 = coords[:,0,:]
# coords_node2 = coords[:,1,:]
# # Swap node1/node2 allocations to make sure the directionality works correctly
# should_swap = coords_node1[:,0] > coords_node2[:,0]
# coords_node1[should_swap], coords_node2[should_swap] = coords_node2[should_swap], coords_node1[should_swap]
# # Distance for control points
# dist = dist_ratio * np.sqrt(np.sum((coords_node1-coords_node2)**2, axis=1))
# # Gradients of line connecting node & perpendicular
# m1 = (coords_node2[:,1]-coords_node1[:,1])/(coords_node2[:,0]-coords_node1[:,0])
# m2 = -1/m1
# # Temporary points along the line which connects two nodes
# # e.g., https://math.stackexchange.com/questions/656500/given-a-point-slope-and-a-distance-along-that-slope-easily-find-a-second-p
# t1 = dist/np.sqrt(1+m1**2)
# v1 = np.array([np.ones(l),m1])
# coords_node1_displace = coords_node1 + (v1*t1).T
# coords_node2_displace = coords_node2 - (v1*t1).T
# # Control points, same distance but along perpendicular line
# # rnd gives the 'polarity' to determine which side of the line the curve should arc
# t2 = dist/np.sqrt(1+m2**2)
# v2 = np.array([np.ones(len(edges)),m2])
# coords_node1_ctrl = coords_node1_displace + (rnd*v2*t2).T
# coords_node2_ctrl = coords_node2_displace + (rnd*v2*t2).T
# # Combine all these four (x,y) columns into a 'node matrix'
# node_matrix = np.array([coords_node1, coords_node1_ctrl, coords_node2_ctrl, coords_node2])
# # Create the Bezier curves and store them in a list
# curveplots = []
# for i in range(l):
# nodes = node_matrix[:,i,:].T
# curveplots.append(bezier.Curve(nodes, degree=3).evaluate_multi(np.linspace(0,1,bezier_precision)).T)
# # Return an array of these curves
# curves = np.array(curveplots)
# return curves
# def draw_curved_graph(G, colors, ax):
# #G = to_networkx(G)
# # layout
# pos = eg.spring_layout(G, iterations=50)
# eg.draw_networkx_nodes(G, pos, ax=ax, node_size=200, node_color=colors[0], alpha=0.5)
# # 绘制标签
# eg.draw_networkx_labels(G, pos, ax=ax, font_size=8, font_family='Arial', font_color='black')
# # Produce the curves
# curves = draw_curved_edges(G, pos)
# lc = LineCollection(curves, color=colors[1], alpha=0.4)
# # 添加连线
# ax.add_collection(lc)
# # 设置坐标轴参数
# ax.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False)
# plt.savefig('Figure.pdf')
# plt.show()